在grpc拦截器中需包装serverstream重写recvmsg,在unmarshal前读取原始http/2帧(含5字节头),提取payload校验完整性;须透传其他方法,显式返回status.error避免暴露内部错误,不可依赖http中间件。

gRPC拦截器里怎么拿到完整的请求body
不能直接拿——UnaryServerInterceptor传入的req参数是反序列化后的结构体,原始字节流在解码后就被丢弃了。想校验完整性(比如验签、摘要、长度限制),必须在解码前介入。
正确做法是包装grpc.ServerStream,重写RecvMsg方法,在数据被Unmarshal前捕获原始[]byte。gRPC本身不提供“解码前钩子”,得自己造一层流代理。
- 拦截器函数签名里只有
ctx、req(已解码)、info、handler,没地方塞原始包体 - 必须实现
grpc.StreamServerInterceptor或改写UnaryServerInterceptor内部对ServerStream的处理逻辑 - Go标准库
grpcv1.60+ 支持grpc.WithStatsHandler,但它是只读统计用,不能修改或拦截payload
用自定义ServerStream包装原始stream做校验
核心思路:在拦截器中把原始grpc.ServerStream替换成一个包装对象,该对象在RecvMsg被调用时先读取底层io.Reader(实际是transport.Stream),提取完整二进制帧,再交给原逻辑解码。
注意gRPC HTTP/2帧结构:每个message前有5字节头(1字节压缩标志 + 4字节长度),真实payload从第6字节开始。校验必须基于这个原始帧,而不是解码后的结构体。
- 不要试图从
ctx里取http.Request——gRPC走HTTP/2,没有传统request body概念 - 包装
ServerStream时需透传所有其他方法(SendMsg、SetHeader等),否则服务端响应会失败 - 若使用
grpc-gov1.58+,可借助transport.Stream的Read方法;老版本需用stream.RecvCompress等字段间接访问 - 示例关键片段:
type wrappedStream struct { grpc.ServerStream recvFunc func(interface{}) error } func (w *wrappedStream) RecvMsg(m interface{}) error { // 这里读原始帧,计算sha256,检查长度... return w.recvFunc(m) }
校验失败时如何中断请求且不暴露内部错误
不能靠return errors.New("xxx")直接抛错——那样gRPC会返回codes.Unknown,客户端难区分是校验失败还是服务崩溃。必须显式返回带codes.InvalidArgument或codes.PermissionDenied的status.Error。
- 用
status.Errorf(codes.InvalidArgument, "body digest mismatch"),客户端status.Code(err)能准确拿到错误类型 - 避免在
RecvMsg里panic——会导致整个stream关闭,可能影响其他并发请求 - 如果校验依赖外部服务(如密钥中心),超时要设短(≤200ms),并降级为允许通过(视安全等级而定),否则拖慢所有请求
- 日志里记录
req.Method()和peer.FromContext(ctx).Addr,但别打原始body,防敏感信息泄漏
为什么不用middleware层(如HTTP中间件)做这件事
因为gRPC不是HTTP API——你加在http.Handler上的中间件,根本收不到gRPC消息体。gRPC over HTTP/2 的payload被grpc-go底层transport直接解析,绕过了net/http的Request.Body。
- 在
grpc.Server启动时绑定的http.Handler只是个“门面”,真实通信由transport.ServerTransport处理 - 哪怕用
grpc-gateway转成REST,那也是另一条路径,原始gRPC调用仍走纯二进制流 - 想统一校验,必须在gRPC协议栈内层动手,不是在应用路由层
ServerStream多次RecvMsg,每次都要重新校验帧头,但buffer管理稍有不慎就会错位。这点容易被忽略。










