红魔咖啡馆

头发越掉越多,头发越掉越少

0%

【gRPC】中间件

洋葱模型

img

洋葱模型中,每一层相当于一个中间件,用于处理特定功能

假设洋葱中间是我们的handler处理程序,则处理顺序应该是:

handler前请求→handler→handler后响应

也就是每个中间件都有两次处理时机

因此我们一般将一些需要前置处理的,比如拦截登录token之类的请求,放在handler前面,而需要后置处理的,比如收集方法耗时之类的信息,放在handler后面,如以下代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
authCtrl := &ctrl.AuthController{}
	s := grpc.NewServer(
		grpc.ChainUnaryInterceptor(
			func(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (resp interface{}, err error) {
				log.Println("login1 before")
				resp, err = handler(ctx, req)
				log.Println("login1 after")
				return resp, err
			},
			func(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (resp interface{}, err error) {
				log.Println("login2 before")
				resp, err = handler(ctx, req)
				log.Println("login2 after")
				return resp, err
			},
			func(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (resp interface{}, err error) {
				log.Println("login3 before")
				resp, err = handler(ctx, req)
				log.Println("login3 after")
				return resp, err
			},
		),
	)

输出为

1
2
3
4
5
6
7
2026/07/16 20:42:04 login1 before
2026/07/16 20:42:04 login2 before
2026/07/16 20:42:04 login3 before
2026/07/16 20:42:04 Login request received: admin admin
2026/07/16 20:42:04 login3 after
2026/07/16 20:42:04 login2 after
2026/07/16 20:42:04 login1 after

功能

gRPC中的中间件是拦截器

UnaryInterceptor

一元拦截器,用于一次请求-一次响应的RPC调用

ChainUnaryInterceptor

这样可以使用多个一元拦截器,如

1
2
3
4
5
6
7
s := grpc.NewServer(
    grpc.ChainUnaryInterceptor(
        RecoveryInterceptor,   // 捕获 panic
        AuthInterceptor,       // 认证鉴权
        LoggingInterceptor,    // 日志
    ),
)

执行顺序按照洋葱模型

例-auth中间件

以下是一个验证登录的中间件,服务端代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
s := grpc.NewServer(
    grpc.UnaryInterceptor(
        auth.UnaryServerInterceptor(func(ctx context.Context)(context.Context, error) {
            // 从ctx中取出token
            md, ok := metadata.FromIncomingContext(ctx)
            if !ok {
                return ctx, status.Error(codes.Unauthenticated, "no Authorized")
            }
            token := md.Get("token")
            key := md.Get("key")
            fmt.Println("token:", token, "key:", key)
            // 验证token
            if (len(token)>0){
                tk := token[0]
                if tk=="123" {
                    // 拦截
                    return ctx, nil
                }
            }
            // 放过
            return ctx, status.Error(codes.Unauthenticated, "no Authorized")
        }),
    ),
)
  • 通过metadata的方法取出需要的中间件
  • 验证token,若长度大于0则取出判断是不是需要的,若是则拦截
  • 否则放过

客户端代码:

1
2
3
4
5
6
md := metadata.Pairs("key", "value","token", "123")
ctx := metadata.NewOutgoingContext(context.Background(), md)
resp,err := cc.GetClient().Login(ctx, &userProto.LoginRequest{
    Username: "admin",
    Password: "admin",
})
  • 使用pair可以创建多个metadata,奇数为key,偶数为value
  • 出站使用outgoing获取带有token的context,并传入login

http请求存在一个header和一个body,body内存储我们的请求消息,而header存储着JWT token,request ID等数据,这些数据就是用metadata来承载的

metadata中存在两种key:

  • incoming:服务内部方法之间传递
  • outgoing:出入当前服务与其他服务传递

gRPC中提供了读取和发送metadata的方法:

  • FromOutgoing/IncomingContext
  • NewIncoming/OutgoingContext
  • AppendToOutgoingContext

new会直接覆盖metadata,append则会追加

若服务端实现了authFuncOverride方法,我们的中间件就会直接调用服务端实现的auth方法

ctrl/middleware.go中实现该方法,就可以绕过authFunc

实现此方法的原因是我们需要对不同的方法进行不同操作,如Login方法不需要鉴权,而其他方法需要

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
func (AuthController) AuthFuncOverride(ctx context.Context, fullMethodName string) (context.Context, error) {
	// 放过登录接口
	if fullMethodName == "/auth.AuthService/Login" {
		return ctx, nil
	}
	// 从ctx中取出token
	md, ok := metadata.FromIncomingContext(ctx)
	if !ok {
		return ctx, status.Error(codes.Unauthenticated, "no Authorized")
	}
	token := md.Get("token")
	key := md.Get("key")
	fmt.Println("token:", token, "key:", key)
	// 验证token
	if (len(token)>0){
		tk := token[0]
		if tk=="123" {
			// 拦截
			return ctx, nil
		}
	}
	// 放过
	return ctx, status.Error(codes.Unauthenticated, "no Authorized")
}

type userInfoKey struct{}	
func newUserContext(ctx context.Context, userinfo *model.UserModel) context.Context {
	return context.WithValue(ctx, userInfoKey{}, userinfo)
}

func userFromContext(ctx context.Context) (*model.UserModel, error) {
	um, ok := ctx.Value(userInfoKey{}).(*model.UserModel)
	if !ok {
		return nil, status.Error(codes.NotFound, "user not found")
	}
	return um, nil
}

链路追踪