最近工作使用gRPC来做通信,一开始在研究认证时,发现了gRPC的拦截器,在这里记录下其用法,后续备忘

拦截器的分类

在gRPC中有两种拦截器UnaryInterceptorStreamInterceptor,其中UnaryInterceptor拦截普通的一次请求一次响应的rpc服务,StreamInterceptor拦截流式的rpc服务。

Server

在包google.golang.org/grpc中,给Server提供了两个适用于Unary和Stream的拦截器函数分别是UnaryInterceptorStreamInterceptor

UnaryInterceptor

UnaryInterceptor函数的唯一参数是一个UnaryServerInterceptor

1
2
3
func UnaryInterceptor(i UnaryServerInterceptor) ServerOption {
......
}

UnaryInterceptor返回的ServerOption作为grpc.NewServer的参数。

而UnaryServerInterceptor是一个函数类型:

1
type UnaryServerInterceptor func(ctx context.Context, req interface{}, info *UnaryServerInfo, handler UnaryHandler) (resp interface{}, err error)

其中四个参数分别是

  • ctx: 当前请求的Context
  • req:当前请求的参数
  • info:当前请求的服务端信息
  • handler:服务端处理这次请求的handler

来自[email protected]的一个简单的例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
// Authorization unary interceptor function to handle authorize per RPC call
func serverInterceptor(ctx context.Context,
req interface{},
info *grpc.UnaryServerInfo,
handler grpc.UnaryHandler) (interface{}, error) {
start := time.Now()
// Skip authorize when GetJWT is requested
if info.FullMethod != "/proto.EventStoreService/GetJWT" {
if err := authorize(ctx); err != nil {
return nil, err
}
}

// Calls the handler
h, err := handler(ctx, req)

// Logging with grpclog (grpclog.LoggerV2)
grpcLog.Infof("Request - Method:%s\tDuration:%s\tError:%v\n",
info.FullMethod,
time.Since(start),
err)

return h, err
}

// authorize function authorizes the token received from Metadata
func authorize(ctx context.Context) error {
md, ok := metadata.FromIncomingContext(ctx)
if !ok {
return status.Errorf(codes.InvalidArgument, "Retrieving metadata is failed")
}

authHeader, ok := md["authorization"]
if !ok {
return status.Errorf(codes.Unauthenticated, "Authorization token is not supplied")
}

token := authHeader[0]
// validateToken function validates the token
err := validateToken(token)

if err != nil {
return status.Errorf(codes.Unauthenticated, err.Error())
}
return nil
}

gRPC支持在Context中发送metadata, google.golang.org/grpc/metadata包提供了操作metadata的功能。

Client

在Client中全局传递metadata

1
2
3
func WithPerRPCCredentials(creds credentials.PerRPCCredentials) DialOption {
......
}

其中参数是个interface:

1
2
3
4
type PerRPCCredentials interface {
GetRequestMetadata(ctx context.Context, uri ...string) (map[string]string, error)
RequireTransportSecurity() bool
}

最后示例代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
// customCredential 自定义认证
type customCredential struct{}
// GetRequestMetadata 实现自定义认证接口
func (c customCredential) GetRequestMetadata(ctx context.Context, uri ...string) (map[string]string, error) {
return map[string]string{
"authorization": "xxxxx",
}, nil
}
// RequireTransportSecurity 自定义认证是否开启TLS
func (c customCredential) RequireTransportSecurity() bool {
return false
}

func main() {
conn, err := grpc.Dial("localhost:50051", grpc.WithInsecure(), grpc.WithPerRPCCredentials(new(customCredential)))
if err != nil {
log.Fatalf("Dial error: %+v\n", err)
}
defer conn.Close()
c := pb.NewExpStreamClient(conn)
r, err := c.RpcMethod(context.Background()....)
if err != nil {
log.Fatalf("say unary error %v\n", err)
}
}

除了通过grpc.WithPerRPCCredentials全局传递metadata外,还可以通过每次context传递metadata

1
2
3
4
5
ctx := context.Background()
md := metadata.Pairs("authorization", jwtToken)
ctx = metadata.NewOutgoingContext(ctx, md)
// Calls RPC method CreateEvent using the stub client
resp, err := client.CreateEvent(context.Background(), event)

go-grpc-middleware

go-grpc-middleware包装了一些列拦截器的链式操作,示例代码如下:

1
2
grpc.UnaryInterceptor(grpc_middleware.ChainUnaryServer(loggingUnary, monitoringUnary, authUnary),
)