gRPC Token 必须通过 metadata.AppendToOutgoingContext(ctx, "authorization", "Bearer "+token) 注入,服务端用 metadata.FromIncomingContext 解析并校验,键名小写、前缀严格匹配,否则因元数据未透传导致鉴权失败。

gRPC Metadata 里怎么安全塞 Token
Token 必须放在 metadata.MD 的 "authorization" 键里(小写),且值要带 "Bearer " 前缀。直接塞裸字符串或用大写键名,服务端 metadata.FromIncomingContext 拿不到。
常见错误现象:ctx.Value(metadata.mdKey{}) 返回空 map;服务端 md.Get("Authorization") 拿到 nil;HTTP/2 层报 UNAUTHENTICATED 但日志没看到 token 解析逻辑。
- 客户端写法必须是:
md := metadata.Pairs("authorization", "Bearer "+token) - 不能用
"Authorization"、"AUTHORIZATION"或自定义键如"x-token"(除非你手动透传且服务端显式读) - gRPC 默认只透传小写 ASCII 键,非标准键会被静默丢弃
客户端如何把 Token 塞进 gRPC 请求上下文
Metadata 不是直接挂在 context.Context 上的,得用 metadata.AppendToOutgoingContext 包一层——否则请求发出去时根本没带元数据。
使用场景:调用前需要鉴权的 unary 或 streaming 方法,比如 client.GetUser(ctx, req)。
立即学习“go语言免费学习笔记(深入)”;
容易踩的坑:有人误用 context.WithValue 往 ctx 塞 map,这完全无效;也有人在 grpc.Dial 时设了 PerRPCCredentials 却忘了实现 GetRequestMetadata 方法。
- 正确写法:
ctx = metadata.AppendToOutgoingContext(ctx, "authorization", "Bearer "+token) - 如果 Token 动态生成(如 JWT 过期需刷新),优先走
credentials.PerRPCCredentials接口,避免每次手动拼 - 注意:这个
ctx必须传给最终的 RPC 调用,不能只传给client.NewXXXClient
服务端怎么从 Context 提取并校验 Token
服务端不能依赖 ctx.Value 直接取,必须用 metadata.FromIncomingContext 解包。它返回的是 (metadata.MD, bool),第二个布尔值必须检查,否则 panic。
性能影响:每次 RPC 都触发一次 map 查找和 slice 遍历,但开销极小;真正耗时的是后续的 JWT 解析和签名校验,这部分别放在 gRPC 拦截器里做重逻辑。
- 必须先判断:
if md, ok := metadata.FromIncomingContext(ctx); ok { ... } - 取值用
md.Get("authorization"),返回[]string,取[0]即可(gRPC 规范要求同键只传一个值) - 拿到字符串后,要 trim 空格、检查是否以
"Bearer "开头,再截取 token 主体——少这步会导致解析失败
为什么 Interceptor 里 Token 有时是空的
最常见原因是客户端没调用 metadata.AppendToOutgoingContext,或者调用了但传的 ctx 没传到最终 RPC 调用点。另一个隐蔽问题是:用了 WithBlock() 或 WithTimeout() 创建新 ctx,却忘了把 metadata 重新 append 进去。
兼容性影响:Go 1.21+ 的 context.WithDeadline 等函数会丢弃所有非标准 context value,而 metadata.MD 就属于这类——所以拦截器里拿到的 ctx 可能“干净”得过分。
- 调试技巧:在拦截器开头加日志,
log.Printf("incoming md: %+v", md),确认是不是空 - 如果用了自定义中间件包装 ctx(比如 trace、logger),必须确保它们调用
metadata.CopyOutgoing或手动透传metadata.MD - 别信文档里“自动继承”的说法——gRPC 的 metadata 是显式透传机制,不是 context 的天然属性
Token 校验逻辑本身不难,难的是 metadata 在 context 树里的生命周期管理。很多人卡在“明明写了 Append 却拿不到”,其实只是漏了一次 context 传递,或者被某层封装悄悄替换了 ctx。










