安全认证


服务开发中需要考虑服务的安全性,如连接是否加密,用户请求是否有权限等,gRPC 支持基于 TLS 的认证保证连接的安全性,通知也支持基于 token 的认证方式,用于对用户做权限认证。

源码目录:

|—- src/
	|-- auth/
		|-- keys/     // 证书目录
		|—— client.go // 客户端
		|—— server.go // 服务端
	|—- protos/ping/
		|—— ping.proto   // protobuf描述文件
		|—— ping.pb.go   // protoc编译生成
    	|-- ping_grpc.pb.go // protoc编译生成

TLS认证

首先需要准备服务端证书,在 keys 目录存放证书文件。

$ cd src/auth/keys
# 生成证书,-config 替换为对应系统的openssl配置文件目录
$ openssl req -newkey rsa:2048 -x509 -nodes -sha256 -days 3650 \
    -keyout server.key -new -out server.crt \
    -subj /CN=grpc.server -reqexts SAN -extensions SAN \
    -config <(cat /System/Library/OpenSSL/openssl.cnf \
        <(printf '[SAN]\nsubjectAltName=DNS:grpc.server'))

生成证书文件后,我们就可以在服务端通过 ServerOption 开启 TLS 了,示例如下:

// src/auth/server.go

func main() {
	creds, err := credentials.NewServerTLSFromFile("keys/server.crt", "keys/server.key")
	if err != nil {
		log.Fatalf("load crt fail:%v", err)
	}
	opts := []grpc.ServerOption{
		grpc.Creds(creds),
	}

	srv := grpc.NewServer(opts...)

...

类似的,在客户端使用 DialOption 开启 TLS,示例如下:

func Ping() {
	// 读取服务端证书,并制定对应服务名
	cred, err := credentials.NewClientTLSFromFile("keys/server.crt", "go-grpc-tutorial")
	if err != nil {
		log.Fatalf("load crt fail: %v", err)
	}

	// 连接配置
	opts := []grpc.DialOption{
		grpc.WithTransportCredentials(cred),
	}
	conn, err := grpc.Dial("localhost:1234", opts...)
	if err != nil {
		log.Fatal(err)
	}

	...

Token 认证

TLS 认证是针对连接的安全加密方式,实际应用中还需要针对每个用户请求进行认证,常用的方式就是基于 token 认证,gRPC 使用 grpc.PerRPCCredentials 接口对此提供了支持。

// PerRPCCredentials defines the common interface for the credentials which need to
// attach security information to every RPC (e.g., oauth2).
type PerRPCCredentials interface {
	// GetRequestMetadata gets the current request metadata, refreshing
	// tokens if required. This should be called by the transport layer on
	// each request, and the data should be populated in headers or other
	// context. If a status code is returned, it will be used as the status
	// for the RPC. uri is the URI of the entry point for the request.
	// When supported by the underlying implementation, ctx can be used for
	// timeout and cancellation. Additionally, RequestInfo data will be
	// available via ctx to this call.
	// TODO(zhaoq): Define the set of the qualified keys instead of leaving
	// it as an arbitrary string.
	GetRequestMetadata(ctx context.Context, uri ...string) (map[string]string, error)
	// RequireTransportSecurity indicates whether the credentials requires
	// transport security.
	RequireTransportSecurity() bool
}

客户端示例如下:

// src/auth/client.go

func Ping() {
	// 增加认证 Dial Option
	conn, err := grpc.Dial("localhost:1234", grpc.WithPerRPCCredentials(CustomAuth{Token: "1234567890"}))
	if err != nil {
		log.Fatal(err)
	}
	defer conn.Close()

	// 实例化客户端并调用
	client := pb.NewPingPongClient(conn)
	res, err := client.Ping(context.Background(), &pb.PingRequest{Value: "ping"})
	if err != nil {
		log.Fatal(err)
	}
	log.Println(res.Value)
}

// CustomAuth 自定义认证类型
type CustomAuth struct {
	Token string
}

// GetRequestMetadata 生成认证信息
func (a CustomAuth) GetRequestMetadata(ctx context.Context, uri ...string) (map[string]string, error) {
	return map[string]string{
		"authorization": a.Token,
	}, nil
}

// RequireTransportSecurity 是否开启 TLS
func (a CustomAuth) RequireTransportSecurity() bool {
	return false
}

这里定义了一个 CustomAuth 类型,并实现了 grpc.PerRPCCredentials 接口的两个方法,通过 grpc.WithPerRPCCredentials 方法转换为 DialOption 类型初始化连接,这样每次 rpc 调用时 token 信息会通过请求的metadata 传输到服务端。

既然是通过 metadata 传输 token 信息,那么服务端认证就非常简单了,可以实现一个拦截器统一处理请求中的 token,示例如下:

// src/auth/server.go
...

// 服务端拦截器 - token 认证
func authInterceptor(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (resp interface{}, err error) {
	md, ok := metadata.FromIncomingContext(ctx)
	if !ok {
		return nil, errors.New("authorization missing")
	}

	var token string
	if auth, ok := md["authorization"]; ok {
		token = auth[0]
	}
	if token != "1234567890" {
		return nil, grpc.Errorf(codes.Unauthenticated, "token invalid")
	}

	// 处理请求
	return handler(ctx, req)
}

// 启动server
func main() {
	srv := grpc.NewServer(grpc.UnaryInterceptor(authInterceptor))

...

以上就是基于 token 的认证方法,还是比较简单的,实际应用中可以根据自己的业务需求生成不同类型的 token,google.golang.org/grpc/credentials/oauth包也对 oauth2 和 jwt 提供了支持,感兴趣可以看一下 oauth.NewOauthAccess(token *oauth2.Token)oauth.NewJWTAccessFromKey(jsonKey []byte) 方法,实际上也是实现了 grpc.PerRPCCredentials 接口。