这篇复盘记录一次 JWT 双票(AT/RT)治理的真实过程。我们要解决的问题很直接:如果刷新令牌在异常场景下被重复使用,系统能不能及时发现并收口。

说明:本文涉及的域名、IP、账号标识、日志字段、会话标识均为脱敏示例,不对应任何真实生产信息。

系统边界如下:

  • 前端:Web SPA(AT 仅存放在内存)
  • 后端:Go + Gin(鉴权与刷新)
  • 会话层:Redis(令牌状态索引)
  • 网关:HTTPS 反向代理

现象与触发条件

在储能云平台的日常安全演练中,我们针对海量边缘网关与前端看板的认证链路进行了渗透测试,其中重点模拟了“旧 RT 被拦截并重复提交”的极端情况。
旧方案虽然有 AT/RT 分层,但在刷新链路上缺少状态约束,导致旧 RT 在短时间窗口内仍可能被并发利用。

脱敏后的事件样式如下:

{
  "event": "auth.refresh.reuse_detected",
  "user_id_masked": "u-***39",
  "session_id": "s-***b1",
  "ip_masked": "10.**.**.21",
  "action": "session_revoked"
}

排查与改造策略:保留 AT 无状态,强化 RT 治理

我们没有改变双票结构,而是在 RT 刷新路径补齐状态管理,核心目标是“可撤销、可审计、可回收”。

第一层:明确令牌职责

  • AT 负责高频访问,走 Authorization: Bearer
  • RT 只通过 HttpOnly Cookie 传输
  • AT 校验仍保持无状态,确保吞吐和扩展性

这样做的好处是:把控制成本留在 RT 通道,不影响 AT 的轻量访问特性。

第二层:将 RT 刷新改为单次可追踪流程

刷新链路采用“校验旧 RT -> 签发新 RT -> 更新状态 -> 失效旧 RT”的顺序,避免并发刷新出现双活。

Redis 键模型(脱敏示例):

rt:active:{jti}           -> { user_id, session_id, exp }  // 核心状态,TTL 跟随 RT 过期时间
rt:session:{session_id}   -> Set<jti>                      // 冗余索引,用于会话级一键清理
rt:deny:{jti}             -> 1 (TTL=剩余有效期)            // 黑名单,拦截被重放的旧票据
user:sessions:{user_id}   -> Set<session_id>               // 用户级管控,支持“踢出其他设备”

对应约束:

  • 同一 RT 只允许成功刷新一次
  • 新 RT 生效后,旧 RT 立即进入 deny 列表
  • 刷新过程加短时锁,避免并发竞态

并发防抖设计(结果复用窗口)

现代前端 SPA 往往会并发发起多个请求,这很容易导致旧 RT 同时触发多次刷新,进而把合法请求误判为重放。为此,我们设计了 5 秒复用窗口:

  • 第一个请求拿到锁并完成 RT 轮换
  • 轮换得到的 AT/RT 对会缓存 5 秒
  • 相同 key 的并发请求在 5 秒内直接复用这组 AT/RT,不再触发二次轮换
  • 超过窗口后恢复严格单次轮换策略

第三层:异常重放后的处置策略

一旦检测到 RT 重放,系统会触发会话级失效并记录审计事件,再由前端回到登录流程。
目标不是“继续尝试修补会话”,而是尽快结束风险会话。

第四层:把登出和高危操作接入服务端吊销

以下操作统一接入会话失效流程:

  • 用户主动退出登录
  • 管理员重置密码
  • 用户状态调整为锁定或禁用

这一步把“只删浏览器 Cookie”升级为“服务端状态真实失效”。


根因总结

这次问题本质上是治理深度不够,而不是单点代码缺陷:

  1. 早期实现关注“签发与验签”,对令牌生命周期管理覆盖不足。
  2. 会话状态能力已有技术预留,但未完整接入刷新主链路。

改造后的收益

这次治理后,链路上最明显的变化有三点:

  1. 会话可以在服务端快速撤销
  2. RT 重放可以被识别并审计
  3. 安全事件可按用户与会话维度快速定位

对业务体验的影响可控:正常请求保持无感刷新,异常场景统一回到重新登录。


过程改进与行动项

1)把令牌生命周期校验纳入发版检查

上线前新增固定检查项:

  • RT 单次使用验证
  • 刷新并发一致性验证
  • 登出/重置密码后的会话失效验证

2)统一安全审计字段

审计日志统一使用脱敏字段:user_id_maskedsession_idjti_prefixip_maskedua_hashrisk_level

3)固定开展重放演练

按月执行一次“RT 重放”演练,持续验证告警、失效和回收链路是否生效。


Phase 2 演进路线(Roadmap):从会话可控到上下文零信任

经过本次改造,我们已经完成 Phase 1 的核心目标:构建了 Redis 会话撤销链路与并发防重放机制。Refresh 流程会实时校验用户状态,账号进入锁定或禁用状态时会即时阻断新票据签发。

针对更高阶的 Token 劫持场景,Phase 2 规划继续向上下文零信任演进:

  • 上下文感知风控:计划引入环境指纹(IP 段 + UA Hash)对比,当 RT 出现异常地域跳变时,自动执行 AT/RT 全量失效,并联动账号冻结与三方二次身份核验。
  • 误杀率治理:考虑到工业现场可能出现 4G/5G 频繁切网、出口漂移等合法抖动,相关策略将先完成风控模型调优,再按灰度方式切入主链路。

结语

认证安全真正考验的,不是签发速度,而是异常发生后的收口能力。
这次改造最大的价值,是让 AT/RT 架构从“能用”走向“可治理”。