vlambda博客
学习文章列表

gRPC基于拦截器模式的认证

点击文末“阅读原文”解锁资料!

Golang在发光
Golang在发光
19篇原创内容
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





往期文章推荐









戳原文,更有料!