下游拿不到 user_id 是因为 authorization header 未显式透传或解析:上游需手动设置 req.header.set("authorization", token),下游需在 middleware 中解析并存入 context,且 nginx 等代理可能过滤 header。

JWT token 在 Go 微服务间透传时为什么下游拿不到 user_id?
因为默认 HTTP Header 传递时,Authorization 字段不会自动被下游服务读取或解析,更不会自动注入到 context 中。Go 的 http.Client 发起请求时,必须显式把上游拿到的 token 塞进 req.Header.Set("Authorization", "Bearer "+token),否则下游 gin.Context 或 echo.Context 根本看不到它。
常见错误现象:parse token failed: token is empty 或解析后 claims["user_id"] 为 nil —— 实际是 header 没传、传了但拼写错(比如写成 authorization 小写)、或中间代理(如 Nginx)默认过滤了带下划线的 header。
- 确保上游在调用下游前,从当前请求的
Authorizationheader 提取完整 token(含Bearer前缀或仅 token 字符串,取决于下游解析逻辑) - 下游框架需统一约定解析位置:推荐只从
Authorizationheader 读,不从 query 或 cookie;避免混用导致鉴权绕过 - Nginx 配置中若启用
underscores_in_headers on;,需确认是否影响Authorization(它不含下划线,但某些自定义 header 如X-Auth-Token会被拦截)
gin-gonic/gin 中如何安全提取并验证 JWT 并透传给业务 handler?
不要在 middleware 里直接 c.JSON(401, ...) 后 return,否则后续 handler 拿不到解析后的用户信息。正确做法是验证通过后,把解析出的 user_id、role 等关键字段存入 c.Request.Context(),再交由下游 handler 使用。
示例关键代码片段:
func JWTMiddleware() gin.HandlerFunc {
return func(c *gin.Context) {
authHeader := c.GetHeader("Authorization")
if authHeader == "" {
c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"error": "missing Authorization header"})
return
}
tokenString := strings.TrimPrefix(authHeader, "Bearer ")
token, err := jwt.Parse(tokenString, func(token *jwt.Token) (interface{}, error) {
if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {
return nil, fmt.Errorf("unexpected signing method: %v", token.Header["alg"])
}
return []byte(os.Getenv("JWT_SECRET")), nil
})
if err != nil || !token.Valid {
c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"error": "invalid token"})
return
}
if claims, ok := token.Claims.(jwt.MapClaims); ok {
// 把必要字段塞进 context,供后续 handler 用
c.Request = c.Request.WithContext(context.WithValue(c.Request.Context(), "user_id", uint64(claims["user_id"].(float64))))
c.Request = c.Request.WithContext(context.WithValue(c.Request.Context(), "role", claims["role"].(string)))
}
c.Next()
}
}
注意:context.WithValue 的 key 类型推荐用自定义类型(如 type ctxKey string),避免字符串冲突;生产环境建议用 jwt.MapClaims 的结构体强转,而非 interface{} 类型断言。
跨服务调用时如何避免 token 泄露或被篡改?
JWT 本身是 base64 编码+签名,不是加密 —— 所有 payload 都可被解码查看。所以绝不能把密码、手机号等敏感字段放进 token;只放最小必要标识(如 user_id、role、exp),且必须校验 exp 和 iat。
- 签名密钥(
JWT_SECRET)必须严格保密,禁止硬编码,建议通过环境变量或 secret manager 注入 - 所有跨服务 HTTP 调用必须走 HTTPS,否则 token 在传输中明文可见
- 下游服务收到 token 后,应校验
aud(audience)字段是否匹配自身服务名,防止 token 被其他服务误用 - 不建议在日志中打印完整 token;如需调试,只打前 8 位 +
***
为什么用标准库 net/http 调用下游时 token 总是 401?
因为 http.NewRequest 默认不继承上游请求的 header;即使你 copy 了整个 *http.Request,其 Header 是 map 类型,浅拷贝后修改原 request 的 header 不会影响副本。
正确做法是显式构造新 request 并设置 header:
req, _ := http.NewRequest("GET", "https://svc-b.example.com/api/v1/profile", nil)
req.Header.Set("Authorization", c.GetHeader("Authorization")) // 复用上游 token
req.Header.Set("Content-Type", "application/json")
// 注意:如果上游用了自定义 header(如 X-Request-ID),也得一并透传,否则链路追踪断裂
req.Header.Set("X-Request-ID", c.GetHeader("X-Request-ID"))
client := &http.Client{}
resp, err := client.Do(req)
更健壮的做法是封装一个 ForwardHeader 工具函数,白名单指定要透传的 headers(避免把 Cookie、Host 等不该传的也带过去);另外,超时控制、重试逻辑、熔断都得在这个 client 层面补全,不能依赖框架默认行为。
透传链路越长,header 丢失或篡改的概率越高;每个服务都该做最小完备性校验 —— 比如没 Authorization 直接拒掉,而不是默默 fallback 到匿名用户。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











