這篇文章記錄一次 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 -> 更新 Redis 狀態 -> 失效舊 RT。

Redis Key 模型(脫敏示例):

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 劫持場景持續演進:

  • 上下文感知風控:規劃引入環境指紋(IP 段 + UA Hash)比對。當 RT 出現異常地域跳變時,自動執行 AT/RT 全量失效,並聯動帳號凍結與第三方二次身分驗證。
  • 誤判治理:考量工業現場常見的 4G/5G 頻繁切網與出口漂移,相關策略會先完成風控模型調優,再以灰度方式逐步切入主鏈路。

結語

認證安全真正的考驗,不在簽發速度,而在異常發生後的收口速度。
這次改造最有價值的地方,是讓 AT/RT 架構從「可用」進一步走向「可治理」。