authmiddleware必须显式注入context,否则下游无法获取user_id;需定义类型安全的ctxkey、调用context.withvalue创建新context,并通过r.withcontext(ctx)或c.request.withcontext(ctx)透传,且限流等中间件必须置于鉴权之后才能读取user_id。

AuthMiddleware 必须显式注入 context,否则下游拿不到 user_id
很多人写完 AuthMiddleware 发现 handler 里用 ctx.Value("user_id") 拿不到值,根本原因是没调用 r.WithContext(ctx)。HTTP 中间件不是自动透传 context 的魔法,必须手动重建请求对象。
关键点:
- 别用字符串字面量当
context.Value的 key,定义type ctxKey string+const userIDKey ctxKey = "user_id" - 校验成功后,用
ctx := context.WithValue(r.Context(), userIDKey, userID)创建新 context - 最后一定要调用
next.ServeHTTP(w, r.WithContext(ctx)),漏掉这句请求就静默失败 - 如果用了 Gin,
c.Request = c.Request.WithContext(ctx)同样不能少
限流中间件必须放在鉴权之后、业务之前
想按用户维度限流(比如每个 user_id 每分钟最多 100 次),就必须在限流逻辑里能读到鉴权中间件塞进 context 的 user_id。顺序错了,limiter.Key = "user:" + userID 就会 panic 或 fallback 到全局限流。
典型错误写法:limitMiddleware(authMiddleware(handler)) —— 这会让限流在鉴权前执行,userID 还没注入。
正确链式顺序:
-
authMiddleware(注入user_id) -
limitMiddleware(从 context 读user_id,构造 Redis key) -
metricsMiddleware(记录带 label 的 Prometheus 指标) - 最终 handler
go-zero 的 rest.Middleware 支持 keyFunc 参数,可传入 func(c *gin.Context) string { return c.GetString("user_id") },但前提是鉴权已执行。
JWT 解析失败时别返回裸 err,要区分 exp、sig、malformed
jwt.ParseWithClaims 失败原因有十几种,但生产环境不能直接把 err.Error() 返回给客户端。比如 token is expired 和 signature is invalid 对应完全不同的安全处理路径:前者可引导前端刷新,后者要告警并冻结密钥。
推荐做法是用类型断言判断具体错误:
if errors.Is(err, jwt.ErrTokenExpired) {
http.Error(w, "token expired", http.StatusUnauthorized)
return
}
if errors.Is(err, jwt.ErrSignatureInvalid) {
log.Warn("invalid signature", "remote_addr", r.RemoteAddr)
http.Error(w, "forbidden", http.StatusForbidden)
return
}
if strings.Contains(err.Error(), "invalid character") {
http.Error(w, "malformed token", http.StatusBadRequest)
return
}
注意:github.com/golang-jwt/jwt/v5 的错误类型比 v4 更规范,errors.Is 可靠性高;v4 里得靠字符串匹配。
metricsMiddleware 要避免 label 组合爆炸
Prometheus 的 CounterVec 如果按 user_id + endpoint + status 打 label,单个服务跑几天就会触发 cardinality explosion,导致 Prometheus OOM 或磁盘爆满。
安全做法:
- 用户维度只用于限流,监控指标用聚合 label:如
method="POST"、endpoint="/api/order"、status_code="2xx" - 敏感字段(如
user_id)绝不打 label,可用Gauge单独暴露当前活跃用户数 - 高频接口(如健康检查)单独忽略或降采样,避免刷屏
- 用
prometheus.Unregister()配合热重载,防止重复注册导致 panic
真正难的不是写中间件,而是让每层中间件都只做一件事、不越界——鉴权不管限流,限流不碰指标,指标不读数据库。链越长,越要守住边界。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











