服务间鉴权必须采用mTLS+JWT混合方案:mTLS确保传输链路加密与客户端证书校验,JWT仅作身份断言,需严格校验iss、aud、exp,隔离用户与服务鉴权逻辑,gRPC场景使用PerRPCCredentials与拦截器,公钥支持热更新与双钥轮换。

服务间鉴权必须用 mTLS + JWT 混合,单靠 JWT 就是裸奔
单纯在 HTTP Header 里传 X-Service-Token 并用 github.com/golang-jwt/jwt/v5 校验,看似能跑通,但只要传输链路没加密,中间人就能截获、重放甚至篡改 token。2026 年还在用纯 JWT 做服务间认证,等于把钥匙贴在玻璃门上。
- mTLS 是底线:必须确保
req.TLS != nil && len(req.TLS.PeerCertificates) > 0,否则直接401,不进后续逻辑 - JWT 只做身份断言:它不负责传输安全,只回答“这个调用来自哪个服务”
- issuer 必须唯一且可信:所有服务只信任
"https://authz.internal"签发的 token,硬编码或从环境变量加载公钥 PEM,绝不远程拉取 - aud 字段必须严格匹配被调用服务名:比如
user-service收到aud: "order-service"的 token,就得拒掉——这是防跨服务 token 复用的关键防线
写 Gin 中间件时,别把用户鉴权和服务鉴权混在一起
很多团队直接复用登录接口的 AuthMiddleware 套在内部路由上,结果发现服务调用总 401。问题出在:用户 JWT 和服务 JWT 的签发源、有效期、scope、校验方式全都不一样。
- 头部字段要隔离:
X-Service-Token专用于服务间通信,绝不用Authorization: Bearer,避免混淆和误触发 - 校验前先过 mTLS 关卡:在
ParseWithClaims之前加检查,if c.Request.TLS == nil || len(c.Request.TLS.PeerCertificates) == 0 { c.AbortWithStatus(401); return } - claims 验证不能偷懒:必须显式比对
iss、aud、exp,且用time.Now().Add(5 * time.Second)容忍时钟偏移,别依赖系统时间绝对一致 - 错误响应不暴露细节:统一返回
401 Unauthorized,body 空或只含通用提示,不写 “aud mismatch” 或 “expired at 2026-01-27T09:30:00Z”
gRPC 场景下,PerRPCCredentials + 拦截器才是正解
HTTP 中间件管不了 gRPC 调用,而直接在每个 handler 里手写校验,既重复又容易漏。gRPC 的标准做法是用 PerRPCCredentials 透传 token,并在服务端用 UnaryInterceptor 统一拦截。
- 客户端透传:实现
GetRequestMetadata方法,从 context 提取X-Service-Token值,注入metadata.MD{"x-service-token": token} - 服务端拦截:在
AuthInterceptor中用metadata.FromIncomingContext(ctx)提取,再走和 HTTP 相同的 JWT + mTLS 校验流程 - 别用
context.WithValue塞原始 token 字符串:只塞解析后的service_id和iss,避免下游误用或日志泄露 - 注意 gRPC 的
codes.Unauthenticated错误码语义清晰,比 HTTP 的 401 更适合跨语言场景
公钥加载和密钥轮换,最容易在线上翻车
服务启动时加载一次公钥看似简单,但一旦授权中心轮换私钥,所有服务就得重启——这在滚动发布或灰度环境中根本不可行。
mallcloud商城基于SpringBoot2.x、SpringCloud和SpringCloudAlibaba并采用前后端分离vue的企业级微服务敏捷开发系统架构。并引入组件化的思想实现高内聚低耦合,项目代码简洁注释丰富上手容易,适合学习和企业中使用。真正实现了基于RBAC、jwt和oauth2的无状态统一权限认证的解决方案,面向互联网设计同时适合B端和C端用户,支持CI/CD多环境部署,并提
立即学习“go语言免费学习笔记(深入)”;
- 启动时不硬编码路径:用环境变量
AUTHZ_PUBLIC_KEY_PATH或配置中心拉取 PEM 内容,支持热更新 - 公钥内容要校验格式:加载后用
jwt.ParseRSAPublicKeyFromPEM尝试解析,失败立刻 panic,别等第一个请求才报错 - 轮换期需双公钥兼容:新旧 key 并存一段时间,JWT header 中的
kid字段标明用哪个 key 验签,服务端根据kid动态选公钥 - 别忽略证书链:mTLS 的 peer certificate 可能带中间 CA,服务端校验时要传入完整 CA bundle,否则
VerifyHostname会静默失败
真实线上最常出问题的,从来不是 JWT 解析代码写得对不对,而是 mTLS 握手是否真成功、公钥有没有及时更新、aud 字段是否随服务名变更同步调整——这些点不盯紧,再漂亮的中间件也只是纸糊的盾。









