GRPC - Interceptor

grpc 服务端提供了 interceptor 功能,可以在服务端接收到请求时优先对请求中的数据做一些处理后再转交给指定的服务处理并响应,功能类似 middleware ,很适合在这里处理验证、日志等流程。在前面的自定义 Token 认证示例中,认证信息是由每个服务中的方法处理并认证的,如果有大量的接口方法,这种姿势就太蛋疼了,每个接口实现都要先处理认证信息。这个时候 interceptor 就站出来解决了这个问题。

修改服务端代码 server/main.go

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
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
package main

import (
"net"
"fmt"

pb "liusha.me/example/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.HelloReply, error) {
resp := new(pb.HelloReply)
resp.Message = "Hello " + in.Name + "."

return resp, nil
}

// auth 验证Token
func auth(ctx context.Context) error {
md, ok := metadata.FromContext(ctx)
if !ok {
return fmt.Errorf("Err%d: 无Token认证信息", codes.Unauthenticated)
}

var (
appid string
appkey string
)

if val, ok := md["appid"]; ok {
appid = val[0]
}

if val, ok := md["appkey"]; ok {
appkey = val[0]
}

if appid != "101010" || appkey != "i am key" {
return fmt.Errorf("Err%d: Token认证信息无效: appid=%s, appkey=%s", codes.Unauthenticated, 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/server.pem", "./keys/server.key")
if err != nil {
fmt.Errorf("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)

fmt.Println("Listen on " + Address + " with TLS + Token + Interceptor")

s.Serve(listen)
}

运行:

1
2
3
go run hello/server/main.go

Listen on 127.0.0.1:50052 with TLS + Token + Interceptor

修改客户端代码 client/main.go

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
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
package main

import (
"os"
"fmt"

pb "liusha.me/example/proto" // 引入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/server.pem", "dev.liusha.me")
if err != nil {
fmt.Errorf("Failed to create TLS credentials %v", err)
os.Exit(1)
}
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 {
fmt.Println(err)
os.Exit(1)
}

fmt.Println(r.Message)
}

运行:

只需要在实例化 server 前注册需要的 interceptor,就可以轻松解决前面提到的问题

1
2
3
go run hello/client/main.go

Hello gRPC.

项目推荐

go-grpc-middleware

该项目对interceptor进行了封装,支持多个拦截器的链式组装,对于需要多种处理的地方使用起来会更方便些。