gRPC 拦截器 Interceptor

gRPC Interceptor

Posted by alovn on July 6, 2019

grpc服务需要提供认证的功能,如果认证信息是由每个服务中的方法处理并认证的,每个接口实现都要先处理认证信息,这种姿势就太蛋疼了。这个时候interceptor就站出来解决了这个问题,可以在请求被转到具体接口之前处理认证信息,一处认证,到处无忧。

grpc服务端提供了interceptor功能,可以在服务端接收到请求时优先对请求中的数据做一些处理后再转交给指定的服务处理并响应,功能类似middleware,很适合在这里处理验证、日志等流程。

在 gRPC 中,大类可分为两种 RPC 方法,与拦截器的对应关系是:

  • 普通方法:一元拦截器(grpc.UnaryInterceptor)
  • 流方法:流拦截器(grpc.StreamInterceptor)

服务端实现, server.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
package main

import (
    "google.golang.org/grpc/grpclog"
    "google.golang.org/grpc/status"
    "net"
    "log"

    pb "grpc_demo/grpc_interceptor/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/metadata" // grpc metadata包
)


// 定义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.Reply = "Hello " + in.Greeting + "."

    return resp, nil
}

// auth 验证Token
func auth(ctx context.Context) error {
    md, ok := metadata.FromIncomingContext(ctx)
    if !ok {
        return status.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]
    }

    if appid != "10001" || appkey != "mysecret" {
        return status.Errorf(codes.Unauthenticated, "Token认证信息无效: appid=%s, appkey=%s", appid, appkey)
    }

    return nil
}

func main() {
    listen, err := net.Listen("tcp", "127.0.0.1:50052")
    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 {
    //    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.RegisterHelloServiceServer(s, HelloService)

    log.Println("Listen on 50052 with Token + Interceptor")

    s.Serve(listen)
}

客户端代码,client.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
package main

import (
    "log"
    pb "grpc_demo/grpc_interceptor/proto" // 引入proto包

    "golang.org/x/net/context"
    "google.golang.org/grpc"
)

const (
    // Address gRPC服务地址
    address = "127.0.0.1:50052"
)

// customCredential 自定义认证
type customCredential struct{}

func (c customCredential) GetRequestMetadata(ctx context.Context, uri ...string) (map[string]string, error) {
    return map[string]string{
        "appid":  "10001",
        "appkey": "mysecret",
    }, nil
}

func (c customCredential) RequireTransportSecurity() bool {
    return false
}

func main() {
    var opts []grpc.DialOption

    opts = append(opts, grpc.WithInsecure())

    // 指定自定义认证
    opts = append(opts, grpc.WithPerRPCCredentials(new(customCredential)))

    conn, err := grpc.Dial(address, opts...)

    if err != nil {
        log.Fatalln(err)
    }

    defer conn.Close()

    // 初始化客户端
    c := pb.NewHelloServiceClient(conn)

    // 调用方法
    reqBody := new(pb.HelloRequest)
    reqBody.Greeting = "gRPC"
    r, err := c.SayHello(context.Background(), reqBody)
    if err != nil {
        log.Fatalln(err)
    }
    log.Printf("Greeting: %s, %v", r.Reply, r.Number)
}

示例代码: grpc_interceptor

可以发现 gRPC 本身只能设置一个拦截器,难道所有的逻辑都只能写在一起?

关于这一点,可以放心。采用开源项目 go-grpc-middleware 就可以解决这个问题

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