grpc拦截器

如果把 gRPC 服务比作一家公司,业务逻辑是员工,那么拦截器就是前台、保安和审计员

  • 保安(认证拦截器):检查你有没有工牌(Token),没工牌不让进。
  • 审计员(日志拦截器):记录谁在什么时候进了哪个房间,待了多久。
  • 前台(熔断/限流):人太多了,先在外面排队。

拦截器的核心优势是:业务代码不需要关心这些杂事,拦截器统一处理。

下面我将通过 3 个循序渐进的 Demo,带你彻底掌握 gRPC 拦截器。


第一部分:前置准备

为了专注讲解拦截器,我们简化 Proto 文件。

1. 定义 Proto (proto/auth.proto)

1
2
3
4
5
6
7
8
9
syntax = "proto3";
package auth;

message PingRequest { string msg = 1; }
message PingResponse { string msg = 1; }

service AuthService {
rpc Ping (PingRequest) returns (PingResponse);
}

(记得运行 protoc 命令生成 auth.pb.goauth_grpc.pb.go)

2. 初始化项目

1
2
3
4
5
6
go mod init example.com/grpc-interceptor-demo
go get google.golang.org/grpc
go get google.golang.org/grpc/credentials/insecure
go get google.golang.org/grpc/metadata
go get google.golang.org/grpc/status
go get google.golang.org/grpc/codes

第二部分:核心概念解析

在写代码前,必须看懂拦截器的函数签名。

服务端一元拦截器签名:

1
func(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (resp interface{}, err error)
  • ctx: 上下文(包含元数据、超时等)。
  • req: 请求参数。
  • info: 当前调用的信息(如方法名 /auth.AuthService/Ping)。
  • handler: 真正的业务逻辑函数如果你不调用它,业务代码永远不会执行!
  • resp, err: 返回给客户端的结果。

第三部分:完整 Demo 演示

Demo 1:日志拦截器 (Logging Interceptor)

目标:记录每个请求的方法名、耗时、是否有错误。
场景:运维排查问题,知道哪个接口慢。

代码文件:interceptors/log.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
package interceptors

import (
"log"
"time"
"context"
"google.golang.org/grpc"
)

// UnaryLogInterceptor 服务端一元日志拦截器
func UnaryLogInterceptor(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (interface{}, error) {
// 1. 请求前:记录开始时间
start := time.Now()
log.Printf("【开始】方法:%s", info.FullMethod)

// 2. 执行真正的业务逻辑 (关键!)
// handler 代表了注册在 Server 里的具体业务函数 (如 Ping)
resp, err := handler(ctx, req)

// 3. 请求后:计算耗时,记录结果
duration := time.Since(start)
if err != nil {
log.Printf("【结束】方法:%s, 耗时:%v, 错误:%v", info.FullMethod, duration, err)
} else {
log.Printf("【结束】方法:%s, 耗时:%v, 状态:成功", info.FullMethod, duration)
}

return resp, err
}

Demo 2:认证拦截器 (Auth Interceptor)

目标:检查请求头中是否包含合法的 authorization 令牌。
场景:保护接口,未登录用户无法访问。

代码文件:interceptors/auth.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
package interceptors

import (
"context"
"google.golang.org/grpc"
"google.golang.org/grpc/metadata"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
"strings"
)

// UnaryAuthInterceptor 服务端一元认证拦截器
func UnaryAuthInterceptor(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (interface{}, error) {
// 1. 获取元数据 (Metadata)
md, ok := metadata.FromIncomingContext(ctx)
if !ok {
return nil, status.Errorf(codes.Unauthenticated, "缺少元数据")
}

// 2. 获取 Token (通常放在 authorization 字段)
// md.Get 返回的是 []string,取第一个值
tokens := md.Get("authorization")
if len(tokens) == 0 {
return nil, status.Errorf(codes.Unauthenticated, "缺少 Token")
}

token := tokens[0]

// 3. 验证 Token (这里硬编码为 "secret-token" 演示)
// 实际项目中会去查数据库或验证 JWT
if !strings.HasPrefix(token, "Bearer ") {
return nil, status.Errorf(codes.Unauthenticated, "Token 格式错误")
}

// 模拟验证逻辑
if token != "Bearer secret-token" {
return nil, status.Errorf(codes.Unauthenticated, "Token 无效")
}

// 4. 验证通过,放行到业务逻辑
return handler(ctx, req)
}

Demo 3:服务端整合与客户端自动注入

目标:将拦截器链式注册,并在客户端自动添加 Token。

1. 服务端 (server/main.go)
重点:使用 grpc.ChainUnaryInterceptor 将多个拦截器串联。

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
package main

import (
"log"
"net"
"example.com/grpc-interceptor-demo/interceptors"
pb "example.com/grpc-interceptor-demo/proto" // 你的 proto 路径
"google.golang.org/grpc"
)

type AuthServiceImpl struct {
pb.UnimplementedAuthServiceServer
}

func (s *AuthServiceImpl) Ping(ctx context.Context, req *pb.PingRequest) (*pb.PingResponse, error) {
log.Println(">>> 业务逻辑执行:Ping 被调用了")
return &pb.PingResponse{Msg: "Pong: " + req.Msg}, nil
}

func main() {
lis, err := net.Listen("tcp", ":50051")
if err != nil {
log.Fatalf("监听失败:%v", err)
}

// 【关键步骤】注册拦截器链
// 执行顺序:请求 -> Log(前) -> Auth -> 业务 -> Auth(后) -> Log(后) -> 响应
// 注意:如果 Auth 失败,业务和 Log(后) 都不会执行
grpcServer := grpc.NewServer(
grpc.ChainUnaryInterceptor(
interceptors.UnaryLogInterceptor, // 1. 日志
interceptors.UnaryAuthInterceptor, // 2. 认证
),
)

pb.RegisterAuthServiceServer(grpcServer, &AuthServiceImpl{})

log.Println("服务端启动 :50051")
grpcServer.Serve(lis)
}

2. 客户端 (client/main.go)
重点:使用客户端拦截器自动给每个请求加上 Token,业务代码无需感知。

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
package main

import (
"context"
"log"
"example.com/grpc-interceptor-demo/interceptors" // 复用拦截器包,或新建 client 拦截器
"example.com/grpc-interceptor-demo/proto"
"google.golang.org/grpc"
"google.golang.org/grpc/credentials/insecure"
"google.golang.org/grpc/metadata"
)

// 客户端一元拦截器:自动注入 Token
func ClientAuthInterceptor(ctx context.Context, method string, req, reply interface{}, cc *grpc.ClientConn, invoker grpc.UnaryInvoker, opts ...grpc.CallOption) error {
// 1. 创建新的元数据
// 注意:不要直接修改传入的 ctx,最好基于它创建新的
md := metadata.Pairs("authorization", "Bearer secret-token")
newCtx := metadata.NewOutgoingContext(ctx, md)

// 2. 调用真正的 invoker (发送请求)
// 使用新的 ctx,这样请求头里就带了 Token
return invoker(newCtx, method, req, reply, cc, opts...)
}

func main() {
// 【关键步骤】客户端注册拦截器
conn, err := grpc.Dial("localhost:50051",
grpc.WithTransportCredentials(insecure.NewCredentials()),
grpc.WithUnaryInterceptor(ClientAuthInterceptor), // 自动注入 Token
)
if err != nil {
log.Fatalf("连接失败:%v", err)
}
defer conn.Close()

client := proto.NewAuthServiceClient(conn)

// 业务代码非常干净,不需要关心 Token 怎么传
resp, err := client.Ping(context.Background(), &proto.PingRequest{Msg: "Hello"})
if err != nil {
log.Fatalf("调用失败:%v", err)
}

log.Printf("收到响应:%s", resp.Msg)
}

第四部分:运行与验证

1. 正常流程

  1. 启动 server
  2. 启动 client
  3. 观察服务端日志
    • 看到 【开始】方法:/auth.AuthService/Ping
    • 看到 >>> 业务逻辑执行:Ping 被调用了
    • 看到 【结束】方法:/auth.AuthService/Ping... 状态:成功
  4. 观察客户端日志
    • 看到 收到响应:Pong: Hello

2. 异常流程(测试认证拦截器)

修改 client/main.go 中的 Token 为 "Bearer wrong-token"

  1. 启动 client
  2. 观察服务端日志
    • 看到 【开始】方法:/auth.AuthService/Ping
    • 没有 看到 >>> 业务逻辑执行 (说明被拦截器挡住了)
    • 看到 【结束】方法:/auth.AuthService/Ping... 错误:rpc error: code = Unauthenticated desc = Token 无效
  3. 观察客户端日志
    • 看到 调用失败:rpc error: code = Unauthenticated ...

第五部分:拦截器执行顺序(洋葱模型)

当你使用 ChainUnaryInterceptor(A, B) 时,执行流程像洋葱一样:

1
2
3
4
5
6
7
8
9
10
11
12
13
请求进来

[拦截器 A 前置逻辑] (例如:记录开始时间)

[拦截器 B 前置逻辑] (例如:检查 Token)

[ 业务逻辑处理 ] (例如:查数据库)

[拦截器 B 后置逻辑] (例如:记录错误)

[拦截器 A 后置逻辑] (例如:记录结束时间)

响应回去

重要提示

  • 如果拦截器 B 返回了 error,则业务逻辑和 B/A 的后置逻辑都不会执行。
  • 顺序很重要:通常 日志/监控 放在最外层(最先执行,最后结束),认证/限流 放在内层(靠近业务)。

第六部分:总结归纳表

概念 API / 函数 作用 备注
服务端拦截器 grpc.UnaryInterceptor 注册单个拦截器 旧写法,不推荐
grpc.ChainUnaryInterceptor 注册拦截器链 推荐,支持多个
grpc.StreamInterceptor 流式拦截器 处理 Stream 类型 RPC
客户端拦截器 grpc.WithUnaryInterceptor 客户端一元拦截 常用于自动注入 Token
grpc.WithStreamInterceptor 客户端流拦截 较少用
核心参数 grpc.UnaryServerInfo 获取方法名 info.FullMethod 很常用
grpc.UnaryHandler 业务逻辑句柄 必须调用 handler(ctx, req)
grpc.UnaryInvoker 客户端调用句柄 客户端拦截器中必须调用
元数据 metadata.FromIncomingContext 服务端读取 Header 用于获取 Token
metadata.NewOutgoingContext 客户端写入 Header 用于发送 Token
错误 status.Error 返回标准错误 拦截器阻断请求时用

最佳实践建议

  1. 不要阻塞太久:拦截器是同步执行的,如果在拦截器里做耗时操作(如复杂数据库查询),会拖慢所有接口。
  2. Panic 恢复:建议写一个 RecoveryInterceptor,在 handler 外层包一层 defer/recover,防止业务代码 panic 导致整个服务崩溃。
  3. 上下文传递:在拦截器中如果需要修改 context(如添加 User 信息),记得调用 handler(newCtx, req) 传下去。
  4. 流拦截器:上述示例均为一元拦截器。流拦截器(StreamServerInterceptor)逻辑更复杂,需要包装 grpc.ServerStream 对象,初学者建议先精通一元拦截器。
[up主专用,视频内嵌代码贴在这]