GRPC教程 6 - gRPC元数据处理与加密认证和拦截中间件
本篇教程我们来讲解gRPC的元数据处理和拦截的中间件
GRPC教程 6 - gRPC元数据处理与加密认证和拦截中间件
元数据处理
首先,先来讲一讲,元数据(metadata),元数据是指在处理RPC请求中,不放在正文的数据,比如身份信息token(不属于业务信息)可以放在元数据中,通常以map[string][]string
的形式存放,key是string,value是[]string类型。gRPC里规定可以存放二进制数据(key以-bin结尾),key不能以grpc开头(因为有grp以grpc开头的数据)。元数据对于gRPC本身是不可见的,我们通常在上下文代码中存放元数据,在Go语言中我们通过https://pkg.go.dev/google.golang.org/grpc/metadata 仓库去访问元数据。
元数据的定义如下
type MD map[string][]string
// 可以看到它的类型是map[string][]string。
我们可以通过metadata.New()或metadata.Pairs去创建一个MD, 或者可以先创建一个MD,然后通过metadata.Join()去往这个MD里面添加元数据。
// md := metadata.New(map[string]string{"key1": "val1"})
md := metadata.Pairs(
"key", "string value",
"key-bin", string([]byte(b)), // 二进制数据在发送前会进行(base64) 编码
// 收到后会进行解码
)
你可以发现MD有一些常见的使用方法
type MD map[string][]string
func (metadata.MD).Append(k string, vals ...string)
func (metadata.MD).Copy() metadata.MD
func (metadata.MD).Delete(k string)
func (metadata.MD).Get(k string) []string
func (metadata.MD).Len() int
func (metadata.MD).Set(k string, vals ...string)
我们可以通过这些方法去对元数据进行一些操作。
发送接收元数据
我们先说说在metadata包中发送元数据的方法,
//此方法可以将元数据添加到已有的ctx中。好处是不会覆盖之前ctx的metadata
metadata.AppendToOutgoingContext(context.Background(), "key1", "value1", "name", "Lixin", "AGE", "21")
// 这个方法metadata.NewOutgoingContext这个方法会覆盖之前crtx中的metadata。
b := "This is bin message"
// md := metadata.New(map[string]string{"key1": "val1"})
md := metadata.Pairs(
"key", "string value",
"key-bin", string([]byte(b)), // 二进制数据在发送前会进行(base64) 编码
// 收到后会进行解码
)
ctx = metadata.NewOutgoingContext(ctx, md)
再来说一说接收metadata的方法
metadata.FromIncomingContext(ctx)
设置header和trailer
Header和trailer可以简单的理解为头和尾。类似于我们调用HTTP方法的头和尾一样属于元数据的一部分。
在gRPC的上下文中,服务端方可以给客户端方设置header和trailer,客户端可以接受header和trailer。
首先介绍服务器端如何发送header和trailer
// 创建和发送 header
header := metadata.New(map[string]string{"header-name": "lixin"})
grpc.SendHeader(ctx, header)
// 创建和发送 trailer
trailer := metadata.Pairs("trailer-age", "21")
grpc.SetTrailer(ctx, trailer)
下面是客户端接收请求的header和trailer
var header, trailer metadata.MD
r, err := client.Hello(
ctx,
&pb.HelloReq{"name": "Lixin"},
grpc.Header(&header), // 将会接收header
grpc.Trailer(&trailer), // 将会接收trailer
)
下面我们做个示例去使用metadata。
// client.go
func main() {
conn, err := grpc.Dial("127.0.0.1:7890", grpc.WithTransportCredentials(insecure.NewCredentials()))
if err != nil {
log.Fatal(err)
}
defer conn.Close()
c := pb.NewGreeteringClient(conn)
metadata.AppendToOutgoingContext(context.Background(), "key1", "value1", "name", "Lixin", "AGE", "21")
ctx := context.Background()
b := "This is bin message"
// md := metadata.New(map[string]string{"key1": "val1"})
md := metadata.Pairs(
"key", "string value",
"key-bin", string([]byte(b)), // 二进制数据在发送前会进行(base64) 编码
// 收到后会进行解码
)
ctx = metadata.NewOutgoingContext(ctx, md)
var header,trailer metadata.MD
resp, err := c.Hello(ctx, &pb.HelloReq{Name: "nn"}, grpc.Trailer(&trailer), grpc.Header(&header))
if err != nil {
log.Fatal(err)
}
fmt.Println(header)
fmt.Println(trailer)
fmt.Println(resp.GetMsg())
}
// server.go
md, ok := metadata.FromIncomingContext(ctx)
if !ok {
log.Println("No metadata!")
}
md = metadata.New(map[string]string{"name": "lixin"})
err := grpc.SendHeader(ctx, md)
if err != nil {
log.Fatal(err)
}
md = metadata.Pairs(
"AGE", "21",
)
err = grpc.SetTrailer(ctx, md)
if err != nil {
log.Fatal(err)
}
return &pb.HelloResp{
Msg: fmt.Sprintf("Hello %s!\n", in.GetName()),
}, nil
拦截中间件
GRPC可以在RPC的Client/Server的基础上提供了拦截器的中间件。拦截器可以在每次调用GRPC调用的时候记录一些信息或者验证用户token(比如响应时间数据等)。
注册拦截器
首先我们之前的文章/视频讲过,GRPC请求分为直接请求和流式请求,当我们使用直接请求的时候对应的就是一元拦截器,流式请求就是流拦截器。我们这里的示例是一元拦截器,就是直接请求GRPC服务拦截器的使用,在客户端和服务器端都可以自定义拦截器。
怎么去使用拦截器呢?
在server端,我们只需要在注册GRPC服务的时候加入对应拦截器(Interceptor)。
s := grpc.NewServer(grpc.UnaryInterceptor(unaryInterceptor))
并且我们需要实现unaryInterceptor 函数类型是
type UnaryServerInterceptor func(ctx context.Context, req interface{}, info *UnaryServerInfo, handler UnaryHandler) (resp interface{}, err error)
在client端呢?我们需要在Dial GRPC服务IP端口的时候,加入grpc.WithUnaryInterceptor(unaryInterceptor)
, 就可以实现客户端的拦截器。
那我们就来实践一下吧, 我们想先在客户端加一个拦截器,比如我们想让每次传输的字节数不超过3,那么就有对应的方法来实现
// unaryInterceptor 客户端一元拦截器
func unaryInterceptor(ctx context.Context, method string, req, reply interface{}, cc *grpc.ClientConn, invoker grpc.UnaryInvoker, opts ...grpc.CallOption) error {
opts = append(opts, grpc.MaxCallSendMsgSize(3))
start := time.Now()
err := invoker(ctx, method, req, reply, cc, opts...)
fmt.Println("reply: ", reply)
end := time.Now()
fmt.Printf("RPC: %s, start time: %s, end time: %s, err: %v\n", method, start.Format("Basic"), end.Format(time.RFC3339), err)
return err
}
服务器端这里什么都不需要做,因为我们只在客户端加一个拦截器,如果传入的字节数超过3,那么会直接返回错误的。
加密处理
参考https://www.liwenzhou.com/posts/Go/gRPC/#autoid-0-9-1
gRPC的加密处理中间件
那如果我们想让中间件有加密处理这一层呢?
// server.go
creds, err := credentials.NewServerTLSFromFile("certs/server.crt", "certs/server.key")
if err != nil {
log.Fatal(err)
}
s := grpc.NewServer(grpc.Creds(creds), grpc.UnaryInterceptor(unaryInterceptor))
// valid 校验认证信息.
func valid(authorization []string) bool {
if len(authorization) < 1 {
return false
}
token := strings.TrimPrefix(authorization[0], "Bearer ")
// 执行token认证的逻辑
// 这里是为了演示方便简单判断token是否与"some-secret-token"相等
return token == "some-secret-token"
}
// unaryInterceptor 服务端一元拦截器
func unaryInterceptor(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (interface{}, error) {
// authentication (token verification)
md, ok := metadata.FromIncomingContext(ctx)
if !ok {
return nil, status.Errorf(codes.InvalidArgument, "missing metadata")
}
if !valid(md["authorization"]) {
return nil, status.Errorf(codes.Unauthenticated, "invalid token")
}
m, err := handler(ctx, req)
if err != nil {
fmt.Printf("RPC failed with error %v\n", err)
}
return m, err
}
// client.go
certs, err := credentials.NewClientTLSFromFile("./certs/server.crt", "")
if err != nil {
log.Fatal(err)
}
conn, err := grpc.Dial("127.0.0.1:7890", grpc.WithTransportCredentials(certs), grpc.WithUnaryInterceptor(unaryInterceptor))
// unaryInterceptor 客户端一元拦截器
func unaryInterceptor(ctx context.Context, method string, req, reply interface{}, cc *grpc.ClientConn, invoker grpc.UnaryInvoker, opts ...grpc.CallOption) error {
var credsConfigured bool
for _, o := range opts {
_, ok := o.(grpc.PerRPCCredsCallOption)
if ok {
credsConfigured = true
break
}
}
if !credsConfigured {
opts = append(opts, grpc.PerRPCCredentials(oauth.NewOauthAccess(&oauth2.Token{
AccessToken: "some-secret-token",
})))
}
opts = append(opts, grpc.MaxCallSendMsgSize(5))
start := time.Now()
err := invoker(ctx, method, req, reply, cc, opts...)
fmt.Println("reply: ", reply)
end := time.Now()
fmt.Printf("RPC: %s, start time: %s, end time: %s, err: %v\n", method, start.Format("Basic"), end.Format(time.RFC3339), err)
return err
}
这样我们就可以通过中间件来判断有没有加密处理了,如果没有加密处理,我们的服务端是直接拒接请求的。
gRPC拦截器和gin的拦截器对比
Gin框架的拦截器呢?
package main
import (
"fmt"
"github.com/gin-gonic/gin"
)
func main() {
r := gin.Default()
count := 0
r.Use(func(ctx *gin.Context) {
if count > 1 {
ctx.JSON(200, gin.H{
"exit": "Sorry.",
})
ctx.Abort()
}
count++
fmt.Println("This is a intercepter.")
})
r.GET("/hello", func(ctx *gin.Context) {
ctx.JSON(200, gin.H{
"Hello": "Lixin",
})
})
r.Run(":7899")
}
总结
元数据处理
拦截器
GRPC的加密处理
GIN和GRPC 拦截器的对比