jwt中服务间调用必须放service_id而非user_id,因需精准校验调用方身份、目标接口路径及策略范围,混用会导致白名单校验失效;用户端请求才携带user_id与role。

服务间权限分配不能只靠 JWT 里的 role 字段做粗粒度判断,必须结合调用方身份(service_id)、目标接口路径、策略生效范围三者动态校验。
JWT 中该放 service_id 还是 user_id?
取决于通信场景:用户端请求带 user_id + role;服务间调用必须带 service_id + 可选 allowed_targets 声明。混用会导致中间件无法区分“谁在调用”和“代表谁调用”,进而绕过白名单检查。
常见错误现象:Parse 出来的 claims 里只有 user_id,但鉴权逻辑却去查 service_id 白名单,结果永远返回 403。
- 用户登录流程:用
user_id、role、exp即可 - 服务 A 调用服务 B:Token 必须含
service_id: "order-service",且服务 B 的中间件只认这个字段 - 密钥不能复用:用户 Token 和服务 Token 应使用不同
secretKey或非对称密钥对,避免泄露后全线失守
Gin 中间件如何安全提取并验证 service_id
不能只依赖 c.GetHeader("Authorization") 然后无脑 Parse —— 缺少前置校验会暴露签名密钥猜测、空 token、格式错乱等攻击面。
实操建议:
- 先检查 Header 是否存在且以
"Bearer "开头,否则直接c.AbortWithStatusJSON(http.StatusUnauthorized, ...) - 调用
jwt.Parse时,keyFunc必须返回具体密钥,不能硬编码[]byte("xxx");推荐从配置中心或环境变量加载 - 解析成功后,立刻检查
claims["service_id"]类型是否为string,防止类型断言 panic - 把
service_id注入c.Request.Context(),后续 handler 用c.MustGet("service_id").(string)安全取值
RBAC 策略怎么落地到接口级而不是服务级
仅校验 service_id 属于白名单,等于给整个服务开了绿灯,实际中订单服务不该有权限删用户数据。
推荐做法是将权限策略绑定到 Gin 路由层级,而非全局中间件:
- 定义结构体如
type PermissionRule struct { ServiceID string; Path string; Method string; Action string } - 在路由注册时显式声明:
r.POST("/users/delete", authz.Middleware("order-service", "DELETE:/users/delete"), handler.DeleteUser) - 中间件内部查策略表(内存 map 或 Redis 缓存),匹配
ServiceID + Method + Path三元组,不匹配则拒访 - 避免用正则匹配路径,性能差且易被绕过;优先用前缀匹配或精确匹配
gRPC 场景下怎么复用同一套权限逻辑
HTTP 和 gRPC 的认证入口不同,但核心校验逻辑(解析 JWT、查策略、注入上下文)可以完全复用。关键差异在拦截器写法。
实操要点:
- gRPC 使用
metadata.FromIncomingContext提取token元数据,不是 Header - 拦截器中解析出
service_id后,调用同一个函数CheckPermission(serviceID, fullMethod),其中fullMethod形如"/grpc.examples.echo.Echo/UnaryEcho" - 不要在拦截器里重复实现 RBAC 查询逻辑 —— 抽成独立包,HTTP 和 gRPC 都 import 它
- 注意 gRPC 错误码:拒绝时返回
status.Error(codes.PermissionDenied, "access denied"),别用 HTTP 状态码
最常被忽略的点是策略缓存更新机制 —— 权限变更后,如果只改数据库没刷新内存 map 或 Redis,服务会持续按旧规则放行或拦截,问题难以复现且排查耗时。建议所有策略读取都走带 TTL 的缓存,并配合配置中心事件触发 reload。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











