這篇文章記錄一次 JWT 雙票(AT/RT)治理的實際經驗。
我們想回答的問題很簡單:當刷新令牌在異常情境下被重放時,系統能不能快速發現並收口。

說明:本文的網域、IP、帳號識別、會話識別與日誌內容皆為脫敏示例,不對應任何真實生產資料。

系統邊界如下:

  • 前端:Web SPA(AT 僅存於記憶體)
  • 後端:Go + Gin(鑑權與刷新)
  • 會話層:Redis(令牌狀態索引)
  • 閘道:HTTPS 反向代理

現象與觸發條件

在一次面向設備接入與 Web 應用認證鏈路的復現測試中,我們模擬了「舊 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 重放演練,持續驗證告警、失效與回收流程是否正常。


後續可選增強項

目前方案已建立 Redis 會話撤銷鏈路與並發防重放機制。Refresh 流程已能即時校驗使用者狀態,帳號一旦鎖定或停用,會立即阻斷新票據簽發。

如果後續需要覆蓋更複雜的 Token 劫持場景,可再加入以下控制:

  • 環境指紋比對:可引入 IP 段與 UA Hash 組合校驗,在 RT 出現異常地域跳變時觸發更嚴格的票據失效與附加驗證。
  • 規則校正:在複雜網路環境下,需先驗證 4G/5G 頻繁切網、出口漂移等合法抖動,再決定是否啟用更嚴格的判定規則。

結語

認證安全的關注點之一,是異常發生後的收口能力。
這次改造補上了 RT 刷新鏈路中的會話撤銷與並發防重放控制。