API 网关的鉴权插件,说起来核心其实就一句话:在路由匹配之后、业务 handler 之前,精准拦截请求,解析并校验 JWT(包括 exp、iss 和签名),把解析出来的上下文信息透传下去,统一响应格式,最后别忘了调用 Abort() 终止流程。但实际落地时,细节远比这复杂。

用 Go 语言做这件事,难点不在于“写个中间件”,而在于如何在请求生命周期中做到精准拦截、正确解析凭证、合理校验策略,以及上下文信息的无缝传递。如果漏掉了 context.WithValue 的透传,或者误判了 401 和 403 状态码的使用场景,那这个插件基本上等于白写了。
鉴权插件必须挂载在路由匹配之后、业务 handler 执行之前
一个常见的误区是,新手喜欢把鉴权逻辑直接塞在 http.ListenAndServe 的顶层 wrapper 里。这么做的后果是,路径都没解析完,自然无法做基于 path 或 method 的细粒度策略控制。
正确的做法,是在 gorilla/mux 或 gin.Engine.Use() 里注册中间件。这里的关键是,要确保它处在 router.ServeHTTP 的调用链里,但又要早于最终的 handler.ServeHTTP 执行。
- 用
gin的话,顺序是:engine.Use(authMiddleware())之后,再注册engine.GET("/api/user", userHandler)。 - 用
net/http配合gorilla/mux,则用router.Use(authMiddleware),而不是直接去 wraphttp.ListenAndServe。 - 错误的示范:
http.Handle("/", authMiddleware(http.DefaultServeMux))。这种写法下,authMiddleware根本拿不到具体的路由变量(比如{id})和 method 元信息,后续的鉴权策略自然无从谈起。
JWT 解析必须校验 exp、iss 和签名,并且禁用 ParseUnverified
开发阶段图省事,用个 jwt.ParseUnverified 糊弄过去,等上了线就等于裸奔。生产环境下的网关,必须老老实实调用 token, err := jwt.ParseWithClaims(rawToken, &CustomClaims{}, keyFunc),并且确保 keyFunc 返回的密钥和签发方完全一致。
exp的过期检查,jwt.StandardClaims会自动触发,但需要确认time.Now().UTC()的时区与签发方一致,否则容易出现时差导致的误判。iss(issuer)必须显式比对。比如只接受"auth-service.example.com",这一步能有效防止伪造的 token 混进来。- 如果用的是 RSA 公钥验签,
keyFunc应该返回*rsa.PublicKey,而不是[]byte或字符串,否则类型不对,签名验证直接失败。 - 另外,错误日志里千万不要打印完整的
rawToken,敏感信息泄露的风险就在这里。
鉴权失败时,别直接 http.Error,要统一响应结构并终止后续中间件
网关鉴权失败,本质上不是“抛异常”,而是“短路请求流”。正确的做法是用 return 退出当前中间件,并写入一个标准的 JSON 响应体。否则,下游的中间件(比如日志、指标收集)仍然会继续执行,更糟糕的是,状态码可能被覆盖成 200,导致前端收到一个错误的成功响应。
func authMiddleware() gin.HandlerFunc {
return func(c *gin.Context) {
tokenString := c.GetHeader("Authorization")
if tokenString == "" {
c.JSON(401, map[string]string{"error": "missing Authorization header"})
c.Abort() // 关键:终止后续中间件
return
}
// ... 验证逻辑
if !valid {
c.JSON(403, map[string]string{"error": "insufficient permissions"})
c.Abort()
return
}
// 成功则注入用户 ID 到 context
c.Set("user_id", claims.UserID)
}
}
c.Abort()在gin中是必须调用的;如果是net/http场景,则要确保不调用next.ServeHTTP。- 响应体的格式要和业务 API 保持一致。比如都统一用
{"code": 403, "message": "...", "data": null}这种结构,前端才能做统一的错误处理。 - 切忌在鉴权失败时返回重定向(
302)或 HTML 页面——API 网关是服务机器的,不是服务浏览器的。
话说回来,真正考验功力的,从来不是解析 JWT 或者查数据库这些基础操作。把“鉴权决策”和“策略配置热加载”、“多租户上下文隔离”这些点耦合起来,才是难点。比如 RBAC 规则从 etcd 动态拉取,又或者不同的 X-Tenant-ID 头对应不同的权限模型——这些扩展点一旦设计成硬编码,后期改起来,比重写还让人头疼。