JWT 更适合前后端分离场景,因其无状态、可携带用户权限信息、支持自动刷新;API Key 适用于机器间调用,缺乏用户上下文与细粒度权限控制能力。

鉴权该用 JWT 还是 API Key
前后端分离场景下,JWT 是更主流、也更契合的方案。它天然支持无状态、可携带用户身份与权限信息(如 role、permissions)、能配合前端自动刷新,而 API Key 更适合机器对机器调用,不带会话上下文,也不方便做用户级权限控制。
如果你的前端是 Vue/React,登录后需要维护用户登录态、展示不同菜单、拦截未授权路由——这些都依赖 token 中的 payload,JWT 能直接解出 user_id 和 roles,API Key 只能查数据库映射,多一次 IO,还无法区分“张三”和“李四”。
容易踩的坑:
- 把
JWT存在localStorage里,导致 XSS 泄露风险;应优先存httpOnly+Secure的 Cookie - 用
HS256签名但密钥硬编码在代码里,或用默认密钥如secret;必须从环境变量读取,且长度 ≥32 字节 - 忽略
exp校验,或校验逻辑写成time.Now().After(claims.ExpiresAt)却没处理时区/漂移,导致 token 提前失效
gin-jwt 中间件怎么配才不漏请求
别自己手写解析逻辑,用 github.com/appleboy/gin-jwt/v2 这个被广泛验证的中间件。它默认只拦截配置的路由组,但新手常误以为“加了中间件就全局生效”,结果 POST /login 也被拦住,永远登不进去。
正确做法是显式排除登录、注册、刷新等免鉴权接口:
authMiddleware, _ := jwt.New(&jwt.GinJWTMiddleware{
Realm: "login required",
Key: []byte(os.Getenv("JWT_SECRET")),
Timeout: time.Hour,
MaxRefresh: time.Hour,
Authenticator: func(c *gin.Context) (interface{}, error) {
// 用户密码校验逻辑
username := c.PostForm("username")
password := c.PostForm("password")
user, err := findUser(username, password)
if err != nil {
return nil, jwt.ErrFailedAuthentication
}
return user, nil
},
Authorizator: func(data interface{}, c *gin.Context) bool {
// 登录成功后,给 context 加上用户信息
if _, ok := data.(*User); ok {
return true
}
return false
},
Unauthorized: func(c *gin.Context, code int, message string) {
c.JSON(code, gin.H{"error": message})
},
})
r := gin.New()
r.POST("/login", authMiddleware.LoginHandler) // 免鉴权
r.GET("/refresh_token", authMiddleware.RefreshHandler) // 免鉴权
r.Use(authMiddleware.MiddlewareFunc()) // 从这行开始,后续所有路由都受控
r.GET("/user/profile", profileHandler) // ✅ 受保护
关键点:
-
LoginHandler和RefreshHandler必须放在MiddlewareFunc()之前注册,否则它们也会被拦截 -
Authorizator返回true后,data会注入到c.Keys["jwt_payload"],业务 handler 里用c.MustGet("jwt_payload")拿 - 不要在
Authenticator里做耗时操作(如查 Redis),它在每次请求都执行
前端传 token,后端怎么接才不丢字段
默认情况下,gin-jwt 只认 Authorization: Bearer <token> 这种格式。但前端如果用 Axios,可能错写成 headers: { token: 'xxx' },或者 Vue Router 导航守卫里漏传 Authorization,后端就收不到 token,直接 401。
解决方案分两步:
- 后端允许 fallback:在初始化
jwt.GinJWTMiddleware时设置TokenLookup: "header:Authorization,cookie:jwt",这样既支持 Header,也支持 Cookie(用于 httpOnly 场景) - 前端统一封装 request 拦截器,确保每个非登录请求都带上:
axios.defaults.headers.common['Authorization'] = 'Bearer ' + localStorage.getItem('token'),并且登录成功后立刻存进localStorage或同步写入 Cookie - 检查 CORS 配置是否放行了
Authorization头:c.Header("Access-Control-Allow-Headers", "Authorization, Content-Type")
一个典型错误是:前端用 fetch 但没加 credentials: 'include',导致带 Cookie 的请求被浏览器静默过滤,后端根本收不到请求头。
casbin 动态权限控制怎么嵌进 Gin 路由
JWT 解出用户角色后,只是知道“你是管理员”,但不知道“你能不能删订单”。这时候得靠 casbin 做 RBAC 或 ABAC 控制。它不能只在登录时查一次,必须在每次请求时动态校验 {user, path, method} 三元组。
推荐用 github.com/casbin/casbin/v2 + Gin 中间件组合:
e, _ := casbin.NewEnforcer("rbac_model.conf", "rbac_policy.csv")
e.LoadPolicy()
// 自定义中间件
func CasbinMiddleWare(e *casbin.Enforcer) gin.HandlerFunc {
return func(c *gin.Context) {
obj := c.Request.URL.Path
act := c.Request.Method
sub := c.GetString("username") // 从 JWT 中间件已注入的 context 取
if !e.Enforce(sub, obj, act) {
c.AbortWithStatusJSON(403, gin.H{"error": "permission denied"})
return
}
c.Next()
}
}
r.Use(CasbinMiddleWare(e))
r.DELETE("/api/orders/:id", deleteOrderHandler)
注意点:
-
obj不能写死成/api/orders/:id,要匹配路由参数通配,比如用c.FullPath()获取/api/orders/:id,或提前注册 policy 时用/api/orders/* - 模型文件
rbac_model.conf里别漏掉[matchers]行,常见错误是复制粘贴时丢了m = r.sub == p.sub && keyMatch(r.obj, p.obj) && r.act == p.act - 策略数据源别用 CSV 文件硬编码,上线后要用 DB adapter(如
gorm-adapter),否则改权限要重启服务
最易被忽略的是:casbin 的 Enforce 是同步阻塞调用,如果策略数据源是远程 DB 且没加缓存,高并发下会成为性能瓶颈。生产环境务必启用 e.EnableCache(true) 并确认缓存刷新机制可靠。


















