refreshtoken机制是jwt安全落地的刚性前提,必须通过jti轮换、redis状态管理、前端promise共享及设备指纹校验实现原子性与不可重放性,缺一即成安全漏洞。

refreshToken 机制不是可选优化项,而是微服务中 JWT 安全落地的刚性前提。单靠 accessToken 短期过期 + 前端计时器刷新,必然导致并发冲突、令牌复用、状态不一致——你在压测或高并发提交场景下看到的“多个 401 后突然 403”或“用户被踢下线两次”,基本都源于此。
为什么不能只靠 time.Now().After(claims.ExpiresAt) 触发刷新
这个判断只看签名里的 exp 字段,但忽略了三个现实问题:
- 多个请求几乎同时到达过期边缘,都判定“该刷新”,各自调
/auth/refresh→ 服务端生成多对新accessToken/refreshToken - 旧
refreshToken还没来得及失效(比如 Redis DEL 操作延迟或失败),就被另一个并发请求复用 → 一次盗用可续期多次 - 前端收到多个响应,
Authorizationheader 被不同 promise 覆盖,后续请求可能携带已作废的 token → 后端校验失败返回 403
Go 后端必须强制 jti 轮换 + Redis 状态管理
refreshToken 不能是无状态字符串,它必须带可验证、可消耗、不可重放的状态。关键动作在 Go 刷新接口里:
- 解析时先校验签名和
exp,再从 payload 提取jti和fingerprint - 查 Redis:
GET refresh_token:{jti},值格式应为user_id:fingerprint;不匹配或不存在直接拒绝 - 查完立刻
DEL refresh_token:{jti}(单次使用) - 生成新
accessToken(exp设为 15–30 分钟)和新refreshToken(exp设为 7 天,且新jti必须全局唯一) - 写入新
refresh_token:{new_jti},值同上,TTL 设为与新refreshToken的exp一致
漏掉 DEL 或复用旧 jti,等于把刷新入口变成无限续期后门。
前端拦截器必须共享 Promise,不能各自重试
所有 HTTP 请求经过统一拦截器,遇到 401 时不立即刷新,而是:
- 检查是否存在正在执行的
refreshPromise;有则 await 它,不发新请求 - 没有则发起
/auth/refresh,把返回新accessToken的 Promise 缓存到全局变量 - 刷新成功后,用新 token 重放原始请求;失败则清空本地凭证(Cookie 或内存 token)
否则你会在 Vue 或 React 里看到:一个按钮点击触发 3 个并行刷新请求,最终只有最后一个生效,前两个的响应被丢弃但 header 已覆盖 —— 表单提交直接 403。
设备指纹(fingerprint)必须稳定且服务端可控
不能只用 r.UserAgent() 或客户端传的 device_id,因为它们易伪造或变更:
- 推荐组合:
sha256(fmt.Sprintf("%s:%s:%s", r.Header.Get("X-Forwarded-For"), r.UserAgent(), r.Header.Get("Sec-Ch-Ua-Platform"))) - 若用反向代理(如 Nginx),确保
X-Forwarded-For可信,或改用真实 IP(如通过X-Real-IP) - 前端首次登录后,服务端下发该指纹哈希值,后续所有
refreshToken请求必须携带一致值,否则拒绝
指纹不绑定,攻击者拿到一个 refreshToken 就能在任意设备上刷出新 token —— 所有“设备登出”功能都会失效。
refreshToken 就从安全机制退化为放大攻击面的漏洞。











