刷新接口必须校验jti并立即redis标记已使用,jwt的refresh_token需含服务端生成uuid作jti,解析后查redis是否存在且为"used",否则setnx原子设值;刷新成功须清旧cookie再设新cookie;用户数据须通过context注入而非c.keys或c.getstring;access_token过期可在2倍有效期内静默刷新。

刷新接口必须校验 jti 且立即标记已使用
不校验 jti 或漏掉 Redis 标记,等于放行无限次 token 复用。JWT 的 refresh_token 必须携带服务端生成的 UUID 作为 jti 字段,不能由前端传入或拼接时间戳。
解析后立刻查 Redis:GET refresh_jti:<code>jti,若存在且值为 "used",直接拒绝;若不存在或为空,执行 SETNX refresh_jti:<code>jti used EX 86400 —— 这步必须原子完成,否则并发刷新会导致双签发。
常见错误现象:用户登出后仍能刷新成功、同一 refresh_token 被多个设备反复使用、日志里出现大量重复 jti 请求但没报错。
刷新成功后必须清旧 Cookie 并设新 Cookie
用 HttpOnly Cookie 存 refresh_token 时,刷新逻辑里不清理旧值,前端下次请求仍会自动带上已失效的 token,导致 401 → 刷新 → 401 循环。
务必在响应前执行两步:
-
c.SetCookie("refresh_token", "", -1, "/", "example.com", true, true)—— 清空旧 Cookie -
c.SetCookie("refresh_token", newRT, int(time.Hour*24*7).Seconds(), "/", "example.com", true, true)—— 写入新值
注意域名和 Path 必须与登录时完全一致,否则浏览器不会覆盖;Secure 和 HttpOnly 参数也要保持一致,否则新 Cookie 可能被忽略。
别从 c.Keys 或 c.GetString() 读用户身份
中间件解析完 JWT 后,必须通过 c.Request.Context() 注入用户数据,例如:c.Request.Context().Value("user_id")。直接写 c.Keys["user_id"] = uid 在高并发下会被其他中间件覆盖,而 c.GetString("user_id") 对非字符串类型(比如 uint64)会静默返回空字符串,不 panic 也不报错,极难排查。
建议封装工具函数:GetUserFromContext(c *gin.Context) (*Claims, bool),统一做类型断言 + nil 检查,避免在每个 handler 里重复写 if v, ok := c.Request.Context().Value("claims").(*Claims); ok && v != nil。
access_token 过期 ≠ 立即要求重新登录
活跃用户只要在 2 * access_token 有效期内发起过任意一次合法请求,就应允许静默刷新。比如 access_token 设为 15 分钟,那用户从首次登录起 30 分钟内都算“活跃”,期间 access_token 过期可自动续期,无需跳转登录页。
实现上,刷新接口返回新 access_token 时,应在响应头中带:Authorization: Bearer <code>newAT,前端拦截响应后更新本地存储;同时服务端不要在刷新逻辑里查用户表或重走登录校验流程——只依赖已解析的 refresh_token.Claims 即可。
容易被忽略的点是:刷新接口本身也得加鉴权中间件,但它鉴权的是 refresh_token,不是 access_token;且该中间件必须跳过对 /refresh 路径的 access_token 校验,否则永远进不来。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











