洋葱模型
洋葱模型中,每一层相当于一个中间件,用于处理特定功能
假设洋葱中间是我们的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/IncomingContextNewIncoming/OutgoingContextAppendToOutgoingContextnew会直接覆盖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
}