如果把 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.go 和 auth_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 interceptorsimport ( "log" "time" "context" "google.golang.org/grpc" ) func UnaryLogInterceptor (ctx context.Context, req interface {}, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (interface {}, error ) { start := time.Now() log.Printf("【开始】方法:%s" , info.FullMethod) resp, err := handler(ctx, req) 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 interceptorsimport ( "context" "google.golang.org/grpc" "google.golang.org/grpc/metadata" "google.golang.org/grpc/codes" "google.golang.org/grpc/status" "strings" ) func UnaryAuthInterceptor (ctx context.Context, req interface {}, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (interface {}, error ) { md, ok := metadata.FromIncomingContext(ctx) if !ok { return nil , status.Errorf(codes.Unauthenticated, "缺少元数据" ) } tokens := md.Get("authorization" ) if len (tokens) == 0 { return nil , status.Errorf(codes.Unauthenticated, "缺少 Token" ) } token := tokens[0 ] if !strings.HasPrefix(token, "Bearer " ) { return nil , status.Errorf(codes.Unauthenticated, "Token 格式错误" ) } if token != "Bearer secret-token" { return nil , status.Errorf(codes.Unauthenticated, "Token 无效" ) } 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 mainimport ( "log" "net" "example.com/grpc-interceptor-demo/interceptors" pb "example.com/grpc-interceptor-demo/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) } grpcServer := grpc.NewServer( grpc.ChainUnaryInterceptor( interceptors.UnaryLogInterceptor, interceptors.UnaryAuthInterceptor, ), ) 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 mainimport ( "context" "log" "example.com/grpc-interceptor-demo/interceptors" "example.com/grpc-interceptor-demo/proto" "google.golang.org/grpc" "google.golang.org/grpc/credentials/insecure" "google.golang.org/grpc/metadata" ) func ClientAuthInterceptor (ctx context.Context, method string , req, reply interface {}, cc *grpc.ClientConn, invoker grpc.UnaryInvoker, opts ...grpc.CallOption) error { md := metadata.Pairs("authorization" , "Bearer secret-token" ) newCtx := metadata.NewOutgoingContext(ctx, md) return invoker(newCtx, method, req, reply, cc, opts...) } func main () { conn, err := grpc.Dial("localhost:50051" , grpc.WithTransportCredentials(insecure.NewCredentials()), grpc.WithUnaryInterceptor(ClientAuthInterceptor), ) if err != nil { log.Fatalf("连接失败:%v" , err) } defer conn.Close() client := proto.NewAuthServiceClient(conn) resp, err := client.Ping(context.Background(), &proto.PingRequest{Msg: "Hello" }) if err != nil { log.Fatalf("调用失败:%v" , err) } log.Printf("收到响应:%s" , resp.Msg) }
第四部分:运行与验证 1. 正常流程
启动 server。
启动 client。
观察服务端日志 :
看到 【开始】方法:/auth.AuthService/Ping
看到 >>> 业务逻辑执行:Ping 被调用了
看到 【结束】方法:/auth.AuthService/Ping... 状态:成功
观察客户端日志 :
2. 异常流程(测试认证拦截器) 修改 client/main.go 中的 Token 为 "Bearer wrong-token"。
启动 client。
观察服务端日志 :
看到 【开始】方法:/auth.AuthService/Ping
没有 看到 >>> 业务逻辑执行 (说明被拦截器挡住了)
看到 【结束】方法:/auth.AuthService/Ping... 错误:rpc error: code = Unauthenticated desc = Token 无效
观察客户端日志 :
看到 调用失败: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
返回标准错误
拦截器阻断请求时用
最佳实践建议
不要阻塞太久 :拦截器是同步执行的,如果在拦截器里做耗时操作(如复杂数据库查询),会拖慢所有接口。
Panic 恢复 :建议写一个 RecoveryInterceptor,在 handler 外层包一层 defer/recover,防止业务代码 panic 导致整个服务崩溃。
上下文传递 :在拦截器中如果需要修改 context(如添加 User 信息),记得调用 handler(newCtx, req) 传下去。
流拦截器 :上述示例均为一元拦截器。流拦截器(StreamServerInterceptor)逻辑更复杂,需要包装 grpc.ServerStream 对象,初学者建议先精通一元拦截器。
[up主专用,视频内嵌代码贴在这]