跳至主要內容

GRPC教程 6 - gRPC元数据处理与加密认证和拦截中间件

离心原创大约 6 分钟tutorialgolanggrpc

本篇教程我们来讲解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/metadataopen in new window 仓库去访问元数据。

元数据的定义如下

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-1open in new window

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 拦截器的对比