Gin默认不带JWT中间件,因其定位是极简HTTP框架,不内置任何认证逻辑;JWT需手动集成,社区方案(如gin-jwt/v2)存在维护不一、算法支持受限(如仅HS256)等问题,故推荐手写可控性强、安全性高的中间件。

为什么 Gin 默认不带 JWT 中间件
Gin 本身是极简 HTTP 框架,gin.Engine 不内置任何认证逻辑,JWT 验证必须手动集成。官方没提供 gin-jwt 这类包,社区方案有多个(如 github.com/appleboy/gin-jwt/v2),但它们维护节奏、错误处理和签名算法支持不一致——比如 v2 版本默认只支持 HS256,若服务端用 RS256 就会报 key is of invalid type。
实际项目中更推荐手写中间件:逻辑清晰、可控性强、调试方便,且避免引入不熟悉依赖带来的隐式行为。
如何写一个安全可用的 JWT 中间件
核心是三件事:解析 token、校验签名与有效期、注入用户信息到 c(*gin.Context)。关键点不是“怎么写”,而是“怎么防绕过”:
- 必须用
SigningMethodHMAC或SigningMethodRSA显式指定算法,不能依赖自动推断(防止 alg=none 攻击) -
ParseWithClaims后必须检查err和token.Valid两个条件,单独判token.Valid会漏掉签名无效但结构合法的 token - 从
Authorization: Bearer xxx提取 token 时,要 trim 空格并验证前缀,否则攻击者传Bearer\txxx可能绕过校验 - 用户 ID 建议从
claims["user_id"](int64)或claims["sub"](string)取,别直接用claims["id"]这种模糊字段名,避免和其他系统字段冲突
示例片段:
func JWTAuth() gin.HandlerFunc {
return func(c *gin.Context) {
authHeader := c.GetHeader("Authorization")
if authHeader == "" {
c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"error": "missing auth header"})
return
}
parts := strings.Split(authHeader, " ")
if len(parts) != 2 || parts[0] != "Bearer" {
c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"error": "invalid auth header format"})
return
}
tokenString := strings.TrimSpace(parts[1])
<pre class="brush:php;toolbar:false;"> token, err := jwt.ParseWithClaims(tokenString, &UserClaims{}, func(t *jwt.Token) (interface{}, error) {
if _, ok := t.Method.(*jwt.SigningMethodHMAC); !ok {
return nil, fmt.Errorf("unexpected signing method: %v", t.Header["alg"])
}
return []byte(os.Getenv("JWT_SECRET")), nil
})
if err != nil || !token.Valid {
c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"error": "invalid or expired token"})
return
}
if claims, ok := token.Claims.(*UserClaims); ok {
c.Set("user_id", claims.UserID)
c.Next()
} else {
c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"error": "invalid claims"})
}
}}
为什么 UserClaims 结构体必须嵌入 jwt.StandardClaims
如果不嵌入 jwt.StandardClaims,ParseWithClaims 内部无法识别 exp、iat 等标准字段,导致 token.Valid 永远为 false —— 即使 token 签名正确、时间也未过期。
常见错误写法:type UserClaims struct { UserID uint },这会让所有过期校验失效。正确写法必须显式组合:
type UserClaims struct {
UserID uint `json:"user_id"`
jwt.StandardClaims
}
注意:StandardClaims 里的 ExpiresAt 是 int64(Unix 秒),不是 time.Time;生成 token 时要赋值 time.Now().Add(24 * time.Hour).Unix(),别直接塞 time.Time。
登录接口返回 token 时要注意什么
前端通常需要把 token 存进 localStorage 或 httpOnly cookie。Gin 里用 c.SetCookie 设置 httpOnly cookie 更安全,但要注意:
-
MaxAge必须设为正数(单位秒),设 0 表示 session cookie(关浏览器即删),设 -1 会导致 cookie 被忽略 -
SameSite推荐用http.SameSiteStrictMode或http.SameSiteLaxMode,避免 CSRF - 如果同时返回 JSON 和写 cookie,顺序无所谓,但不要在写 cookie 后再调
c.Abort(),否则 cookie 可能不下发 - 别把密钥硬编码进代码,用
os.Getenv("JWT_SECRET"),本地开发可配合.env文件(用godotenv.Load()加载)
生成 token 示例:
token := jwt.NewWithClaims(jwt.SigningMethodHS256, UserClaims{
UserID: user.ID,
StandardClaims: jwt.StandardClaims{
ExpiresAt: time.Now().Add(24 * time.Hour).Unix(),
IssuedAt: time.Now().Unix(),
Issuer: "myapp",
},
})
tokenString, _ := token.SignedString([]byte(os.Getenv("JWT_SECRET")))
真正上线时,ExpiresAt 值应根据业务敏感度调整,管理后台建议 2 小时,普通用户可设 7 天,但必须配套实现 token 黑名单或短生命周期+refresh token 机制。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











