gRPC基于拦截器模式的认证
点击文末“阅读原文”解锁资料!
Golang在发光Golang在发光Official Account
gR PC的服务端需要与认证平台对接, 之前使用http时通过中间件的形式进行实现, 因此这篇文章主要验证gRPC中能否以中间件的形式实现gRPC的认证。
1.认证方式
gRPC 默认提供了两种认证方式:
基于SSL/TLS认证方式
远程调用认证方式
为了保证API Gateway与后端gRPC服务通信的安全同时保证token安全, 以上2种方式同时使用。
2.代码示例
我需要验证的流程大致如下(与Openstack的认证流程一样)
在进行coding前, 我们需要为服务端生成TLS需要的证书:
自建CA
# 生成CA自己的私钥
$(umask 077; openssl genrsa -out private/cakey.pem 2048)
# 自签10年
$openssl req -new -x509 -key private/cakey.pem -out cacert.pem
Country Name (2 letter code) [AU]:CN
State or Province Name (full name) [Some-State]:SiChuan
Locality Name (eg, city) []:ChengDu
Organization Name (eg, company) [Internet Widgits Pty Ltd]:defineIOT Ltd
Organizational Unit Name (eg, section) []:Tec
Common Name (e.g. server FQDN or YOUR name) []:ca
Email Address []:[email protected]
# 初始化自建CA的一部分文件
$mkdir certs newcerts crl
$touch index.txt
$touch serial
$echo 01 > serial
签发服务端证书
# 生成server自己的私钥
$openssl genrsa -out server1.key 2048
# 生成证书签证请求
$openssl req -new -key server1.key -out server1.csr
Country Name (2 letter code) [AU]:CN
State or Province Name (full name) [Some-State]:Chengdu
Locality Name (eg, city) []:Chengdu
Organization Name (eg, company) [Internet Widgits Pty Ltd]:defineIOT Ltd
Organizational Unit Name (eg, section) []:Tec
Common Name (e.g. server FQDN or YOUR name) []:server1
Email Address []:[email protected]
Please enter the following 'extra' attributes
to be sent with your certificate request
A challenge password []:
An optional company name []:
# 因为我CA就在本服务器上,直接签发证书
$openssl ca -in GoWorkDir/src/golang/grpc-auth/keys/server1.csr -out server1.pem -days 3650
Using configuration from /System/Library/OpenSSL/openssl.cnf
Check that the request matches the signature
Signature ok
The stateOrProvinceName field needed to be the same in the
CA certificate (SiChuan) and the request (Chengdu)
# 将签好的证书交给server
$mv server1.pem GoWorkDir/src/golang/grpc-auth/keys
# server端的证书准备完成
$ll GoWorkDir/src/golang/grpc-auth/keys
total 40
-rw-r--r-- 1 maojun staff 1.6K 8 8 09:15 cacert.pem
-rw-r--r-- 1 maojun staff 1.0K 8 8 09:27 server1.csr
-rw-r--r-- 1 maojun staff 1.6K 8 7 21:21 server1.key
-rw-r--r-- 1 maojun staff 4.5K 8 8 09:28 server1.pem
2.1 目录结构
生成契约文件
$protoc --go_out=plugins=grpc:. hello.proto
目录结构如下:
$tree .
.
├── client
│ └── main.go
├── keys
│ ├── cacert.pem
│ ├── server1.csr
│ ├── server1.key
│ └── server1.pem
├── proto
│ ├── hello.pb.go
│ └── hello.proto
└── server
└── main.go
2.2 tls
先看credentials.go中关于通过TLS创建客户端和服务端相关函数
// NewClientTLSFromFile 传入客户端建立TLS连接时需要的证书, 这里主要指CA的证书
// serverNameOverride 仅仅由于测试, 通常传入""
func NewClientTLSFromFile(certFile, serverNameOverride string) (TransportCredentials, error) {
b, err := ioutil.ReadFile(certFile)
if err != nil {
return nil, err
}
cp := x509.NewCertPool()
if !cp.AppendCertsFromPEM(b) {
return nil, fmt.Errorf("credentials: failed to append certificates")
}
return NewTLS(&tls.Config{ServerName: serverNameOverride, RootCAs: cp}), nil
}
// NewServerTLSFromFile 传入服务端建立TLS连接时需要的证书, 这里主要指服务端的证书和私钥
func NewServerTLSFromFile(certFile, keyFile string) (TransportCredentials, error) {
cert, err := tls.LoadX509KeyPair(certFile, keyFile)
if err != nil {
return nil, err
}
return NewTLS(&tls.Config{Certificates: []tls.Certificate{cert}}), nil
}
因此我们自建一个CA, 然后生成server端的证书就可以使用这组函数来完成TLS的建立了
2.2.1 服务端TLS启动
package main
import (
"net"
pb "golang/grpc-auth/proto"
"golang.org/x/net/context"
"google.golang.org/grpc"
"google.golang.org/grpc/credentials" // 引入grpc认证包
"google.golang.org/grpc/grpclog"
)
const (
// Address gRPC服务地址
Address = "127.0.0.1:50052"
)
// 定义helloService并实现约定的接口
type helloService struct{}
// HelloService ...
var HelloService = helloService{}
func (h helloService) SayHello(ctx context.Context, in *pb.HelloRequest) (*pb.HelloResponse, error) {
resp := new(pb.HelloResponse)
resp.Message = "Hello " + in.Name + "."
return resp, nil
}
func main() {
listen, err := net.Listen("tcp", Address)
if err != nil {
grpclog.Fatalf("failed to listen: %v", err)
}
// TLS认证
creds, err := credentials.NewServerTLSFromFile("../keys/server1.pem", "../keys/server1.key")
if err != nil {
grpclog.Fatalf("Failed to generate credentials %v", err)
}
// 实例化grpc Server, 并开启TLS认证
s := grpc.NewServer(grpc.Creds(creds))
// 注册HelloService
pb.RegisterHelloServer(s, HelloService)
grpclog.Println("Listen on " + Address + " with TLS")
s.Serve(listen)
}
2.2.2 客户端带证书调用
package main
import (
pb "golang/grpc-auth/proto"
"golang.org/x/net/context"
"google.golang.org/grpc"
"google.golang.org/grpc/credentials" // 引入grpc认证包
"google.golang.org/grpc/grpclog"
)
const (
// Address gRPC服务地址
Address = "127.0.0.1:50052"
)
func main() {
// TLS连接
creds, err := credentials.NewClientTLSFromFile("../keys/cacert.pem", "server1")
if err != nil {
grpclog.Fatalf("Failed to create TLS credentials %v", err)
}
conn, err := grpc.Dial(Address, grpc.WithTransportCredentials(creds))
if err != nil {
grpclog.Fatalln(err)
}
defer conn.Close()
// 初始化客户端
c := pb.NewHelloClient(conn)
// 调用方法
reqBody := new(pb.HelloRequest)
reqBody.Name = "gRPC"
r, err := c.SayHello(context.Background(), reqBody)
if err != nil {
grpclog.Fatalln(err)
}
grpclog.Println(r.Message)
}
2.3 认证拦截器
认证包含2部分:
服务端认证token
客户端携带token
2.3.1 服务端认证token
拦截器部分的源码在interceptor.go中, 我仅关注普通rpc, 对于流式rpc的拦截器不做说明, 以下是相关函数:
客户端拦截器
服务端拦截器
// UnaryClientInterceptor拦截在客户端执行的非流式RPC. inovker就是真正的RPC的handler,
// 拦截器的责任就是完成自己的逻辑后调用该handler, 让请求继续RPC的工作
type UnaryClientInterceptor func(ctx context.Context, method string, req, reply interface{}, cc *ClientConn, invoker UnaryInvoker, opts ...CallOption) error
// UnaryServerInterceptor 提供了一个在服务器上执行unary RPC的钩子,
// info 包含拦截器可以操作的RPC的所有信息。
// handler 是服务方法实现的一个包装器, 而拦截器的责任就是调用该handler完成RPC, 让请求继续RPC的工作
type UnaryServerInterceptor func(ctx context.Context, req interface{}, info *UnaryServerInfo, handler UnaryHandler) (resp interface{}, err error)
因此我们想要在服务端实现请求的认证功能, 仅需要实现一个自己的UnaryServerInterceptor函数, 并且在server启动时作为参数传递给它即可
总体需要3步:
自定义auth函数,实现认证逻辑
定义一个使用自定义认证(auth)的拦截器
server启动时随参数传入
package main
import (
"net"
pb "golang/grpc-auth/proto"
"golang.org/x/net/context"
"google.golang.org/grpc"
"google.golang.org/grpc/codes" // grpc 响应状态码
"google.golang.org/grpc/credentials" // grpc认证包
"google.golang.org/grpc/grpclog"
"google.golang.org/grpc/metadata" // grpc metadata包
)
const (
// Address gRPC服务地址
Address = "127.0.0.1:50052"
)
// 定义helloService并实现约定的接口
type helloService struct{}
// HelloService ...
var HelloService = helloService{}
func (h helloService) SayHello(ctx context.Context, in *pb.HelloRequest) (*pb.HelloResponse, error) {
resp := new(pb.HelloResponse)
resp.Message = "Hello " + in.Name + "."
return resp, nil
}
// auth 验证Token
func auth(ctx context.Context) error {
md, ok := metadata.FromContext(ctx)
if !ok {
return grpc.Errorf(codes.Unauthenticated, "无Token认证信息")
}
var (
appid string
appkey string
)
if val, ok := md["appid"]; ok {
appid = val[0]
}
if val, ok := md["appkey"]; ok {
appkey = val[0]
}
grpclog.Printf("appid: %s, appkey: %s\n", appid, appkey)
if appid != "101010" || appkey != "i am key" {
return grpc.Errorf(codes.Unauthenticated, "Token认证信息无效: appid=%s, appkey=%s", appid, appkey)
}
return nil
}
func main() {
listen, err := net.Listen("tcp", Address)
if err != nil {
grpclog.Fatalf("Failed to listen: %v", err)
}
var opts []grpc.ServerOption
// TLS认证
creds, err := credentials.NewServerTLSFromFile("../keys/server1.pem", "../keys/server1.key")
if err != nil {
grpclog.Fatalf("Failed to generate credentials %v", err)
}
opts = append(opts, grpc.Creds(creds))
// 注册interceptor
var interceptor grpc.UnaryServerInterceptor
interceptor = func(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (resp interface{}, err error) {
err = auth(ctx)
if err != nil {
return
}
// 继续处理请求
return handler(ctx, req)
}
opts = append(opts, grpc.UnaryInterceptor(interceptor))
// 实例化grpc Server
s := grpc.NewServer(opts...)
// 注册HelloService
pb.RegisterHelloServer(s, HelloService)
grpclog.Println("Listen on " + Address + " with TLS + Token + Interceptor")
s.Serve(listen)
}
2.3.2 客户端携带token
而至于客户端如何在每次调用时都传递自己的token信息, 有比客户端拦截器更方便的方式, 因为credentials中有提供这样的接口 这部分代码在credentials.go中
// PerRPCCredentials 为认证定义了一个通用接口, 每次RPC调用都需要提供安全信息(比如oauth2的token)
type PerRPCCredentials interface {
// GetRequestMetadata 获取当前请求的元数据, 如果需要可以刷新tokens.
// 该方法在请求被传输之前调用, 而数据需要放在header里面或者其他context中.
// uri代表请求条目的URI
// 在底层实现的支持下, ctx可以用于超时和取消
// TODO: 定义限定键的集合,而不是将其保留为任意字符串。
GetRequestMetadata(ctx context.Context, uri ...string) (map[string]string, error)
// RequireTransportSecurity 表明认证过程是否需要安全传输(是否开启TLS)
RequireTransportSecurity() bool
}
因此可以看出我们仅需要实现GetRequestMetadata和RequireTransportSecurity即可, 通过GetRequestMetadata方法将需要的token传递给客户端即可。
package main
import (
pb "golang/grpc-auth/proto"
"golang.org/x/net/context"
"google.golang.org/grpc"
"google.golang.org/grpc/credentials" // 引入grpc认证包
"google.golang.org/grpc/grpclog"
)
const (
// Address gRPC服务地址
Address = "127.0.0.1:50052"
// OpenTLS 是否开启TLS认证
OpenTLS = true
)
// customCredential 自定义认证
type customCredential struct{}
func (c customCredential) GetRequestMetadata(ctx context.Context, uri ...string) (map[string]string, error) {
return map[string]string{
"appid": "101010",
"appkey": "i am key",
}, nil
}
func (c customCredential) RequireTransportSecurity() bool {
if OpenTLS {
return true
}
return false
}
func main() {
var err error
var opts []grpc.DialOption
if OpenTLS {
// TLS连接
creds, err := credentials.NewClientTLSFromFile("../keys/cacert.pem", "server1")
if err != nil {
grpclog.Fatalf("Failed to create TLS credentials %v", err)
}
opts = append(opts, grpc.WithTransportCredentials(creds))
} else {
opts = append(opts, grpc.WithInsecure())
}
// 指定自定义认证
opts = append(opts, grpc.WithPerRPCCredentials(new(customCredential)))
conn, err := grpc.Dial(Address, opts...)
if err != nil {
grpclog.Fatalln(err)
}
defer conn.Close()
// 初始化客户端
c := pb.NewHelloClient(conn)
// 调用方法
reqBody := new(pb.HelloRequest)
reqBody.Name = "gRPC"
r, err := c.SayHello(context.Background(), reqBody)
if err != nil {
grpclog.Fatalln(err)
}
grpclog.Println(r.Message)
}
3.测试
针对以上功能做测试
3.1 正常测试
带证书和合法token的请求
$go run main.go
2017/08/08 10:36:10 Listen on 127.0.0.1:50052 with TLS + Token + Interceptor
2017/08/08 10:36:13 appid: 101010, appkey: i am key
请求成功
$go run main.go
2017/08/08 10:36:13 Hello gRPC.
3.2 异常测试
不带证书的请求
$go run main.go
2017/08/08 11:05:36 Listen on 127.0.0.1:50052 with TLS + Token + Interceptor
2017/08/08 11:05:38 grpc: Server.Serve failed to complete security handshake from "127.0.0.1:56310": tls: first record does not look like a TLS handshake
请求失败
$go run main.go
2017/08/08 11:05:38 transport: http2Client.notifyError got notified that the client transport was broken unexpected EOF.
2017/08/08 11:05:38 rpc error: code = Internal desc = transport is closing
exit status 1
带证书但token不合法
$go run main.go
2017/08/08 11:08:36 rpc error: code = Unauthenticated desc = Token认证信息无效: appid=101010, appkey=i am key1
exit status 1
往期文章推荐