直接用golang-jwt/jwt/v5手写jwt中间件更可控、轻量且易调试,因其仅实现解析bearer token、验证签名与过期、提取并注入user_id三步核心逻辑,避免gin-jwt封装过深、调用时机不透明、双模式混淆、刷新逻辑强耦合及全局超时配置等冗余设计。

直接用 golang-jwt/jwt/v5 自己写中间件,比引入 gin-jwt 更可控、更轻量,也更容易调试和定制。
为什么不用 gin-jwt 中间件
它封装太深,Authenticator 和 PayloadFunc 的调用时机不透明,出错时堆栈难定位;默认支持 Cookie + Header 双模式,但多数前后端分离项目只用 Header,反而增加混淆;刷新 Token 逻辑强耦合,实际业务中常需自定义(比如按角色开关、绑定设备指纹)。
- 你真正需要的只是:解析
Authorization: Bearer xxx→ 验证签名与过期 → 提取用户 ID → 存入c.Set("user_id", id) - 其余逻辑(登录发 token、登出清状态、权限判断)应由业务层决定,不该被中间件绑架
-
gin-jwt的Timeout和MaxRefresh是全局配置,无法按接口粒度控制(比如 /admin 接口要求 15 分钟过期,/user/info 允许 2 小时)
手动实现 JWT 中间件的最小闭环
核心就三个函数:GenerateToken、ParseToken、AuthMiddleware。全部放在 utils/jwt.go 即可,不依赖任何第三方中间件包。
-
GenerateToken:接收user_id和username,返回string类型 token,使用HS256签名,exp设为time.Now().Add(1 * time.Hour).Unix() -
ParseToken:接收tokenStr string,返回user_id int和error;必须显式检查err == nil且claims.VerifyExpiresAt(time.Now(), true) == true -
AuthMiddleware:从c.Request.Header.Get("Authorization")提取 token,跳过空值或非Bearer前缀的情况,验证失败统一返回401
示例片段:
func AuthMiddleware() gin.HandlerFunc {
return func(c *gin.Context) {
authHeader := c.Request.Header.Get("Authorization")
if authHeader == "" || !strings.HasPrefix(authHeader, "Bearer ") {
c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"msg": "missing or malformed Authorization header"})
return
}
tokenStr := strings.TrimPrefix(authHeader, "Bearer ")
userID, err := utils.ParseToken(tokenStr)
if err != nil {
c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"msg": "invalid or expired token"})
return
}
c.Set("user_id", userID)
c.Next()
}
}
容易踩的坑:时钟偏差和密钥管理
生产环境最常遇到的不是代码逻辑错,而是部署时的隐性问题。
- 服务器时间不同步会导致
token is expired或token used before issued—— 必须在jwt.ParseWithClaims时传入jwt.WithLeeway(60 * time.Second),不能只靠VerifyExpiresAt -
Key绝对不能硬编码在代码里,哪怕只是开发环境也要从os.Getenv("JWT_SECRET")读取;v5 版本要求传any类型,所以是func(token *jwt.Token) (any, error) { return []byte(secret), nil } - 别用
jwt.RegisteredClaims直接存敏感字段(如密码哈希),它会被 Base64 编码明文暴露在 token 中 —— 自定义结构体并只放必要字段(user_id,exp,nbf)
权限控制不要塞进中间件
JWT 中间件只做身份认证(Authentication),不做权限判断(Authorization)。后者应该由独立中间件或 handler 内部完成。
- 比如管理员接口,应在路由 handler 里写
if userID != 1 { c.AbortWithStatus(403) },而不是在 JWT 中间件里查数据库判断角色 - 若需 RBAC,建议用单独的
RoleMiddleware("admin"),和 JWT 中间件组合使用:group.Use(AuthMiddleware(), RoleMiddleware("admin")) - JWT payload 里只存
user_id,角色信息由业务查询 DB 获取,避免 token 过期前权限变更无法实时生效
真正的轻量,不在于代码行数少,而在于每个环节都可替换、可测试、不隐藏副作用。token 解析失败时你得知道是密钥错了,还是时间歪了,还是前端漏传了 Bearer —— 这些细节,只有自己写的中间件才给得清清楚楚。











