RFC:密碼登入請求限流與 Cache 故障降級
accepted;2026-10-11 框架維護者(使用者)接受方案 A,已發布
4.0.0-alpha.33。實作已通過人審並完成收尾;發布驗證見 Decision Log,alpha.32 及更早版本不含新 limiter。
問題與流程
應用要求同一 IP+正規化 username,一分鐘前五次密碼登入請求放行,第六次回 429 並告知重試時間。LoginLockout 只累計密碼失敗,不能承擔所有請求配額。
另一既有缺陷是 loginAttempts cache.get/put/remove 的 RuntimeException 導致登入 500。應用要求 cache 暫時故障可繼續登入,提供故障/恢復事件;密碼、帳號資格、登入政策與 SQL Refresh Session 撤銷仍須生效。
下游 alpha.27 的兩項真實 HTTP/Security/H2 測試於 2026-10-11 重跑:2 執行、2 失敗、0 skipped,分別得到 200 而非 429、500 而非 200。XML 時戳 2026-10-11T09:41:11.917Z;只在正式 cache bean 邊界注入故障,非 Redis 網路實測。框架基線 6434b047(alpha.32)同樣缺少此能力。
| 工作 | 處理 |
|---|---|
| 修正 LoginLockout cache 例外傳播並沿用 AuditApplicationEvent 提供事件 | 使用者已授權修正、測試與 peer review,不以全面 RFC 接受阻擋 |
| 新增密碼登入限流公開 API、預設參考接線 | 已接受方案 A,納入目標 alpha.33;應用接線須另外合併 |
| token 黑名單、M2M、OIDC/signed-link、SQL Session 模型 | 本輪維持既有行為 |
不建立通用 CacheFailurePolicy、新故障事件型別或新的通知平台。
新公開契約
public interface PasswordLoginRateLimiter {
Decision evaluate(String clientIp, String username);
record Decision(boolean allowed, int limit, int remaining,
Instant resetAt, long retryAfterSeconds,
boolean degraded) {}
}
CachePasswordLoginRateLimiter 接受 Cache<String, Window> 與 Clock,預設五次/60 秒;另有 limit/Duration/Clock 建構器。Window(List<Instant> acceptedAt) 為不可變、可序列化 cache 值。框架不自動配置;HTTP 與用途 cache 由應用接線。
穩定優先與公開型別範圍
獨立 PasswordLoginRateLimiter 避免把所有請求配額混入既有失敗鎖定,也避免擴張 M2M 的 boolean 契約。Decision 將允許、重試與降級結果集中在單一明確結果;Window 必須公開,讓應用以型別化 Cache bean 接線預設實作。這三個型別皆為新增、已接受且納入目標 alpha.33,不改既有呼叫者或自動加入其他 認證路徑;LoginLockout 原建構器及 subclass 接點保留。
本機有界 cache 是此階段的最小參考實作;不引入分散式平台、通用 CacheFailurePolicy 或可靠通知 設施。新增 API 仍有長期維護成本;工作區完成可審閱實作不等於接受公開契約。維護者已於 2026-10-11 明示接受方案 A,公開型別與以下語意均依本 RFC 決策,目標為 alpha.33。
視窗、身分與計數
採滾動視窗:清除 timestamp ≤ now−window,滿額後拒絕,下一個名額時間為最早 timestamp+window。被拒請求不延長等待,成功登入不清除配額。cache expiry 僅用於清理,不以 TTI 滑動等待時間。
合法形狀請求在密碼驗證前計數,正確密碼、錯誤密碼及未知帳號皆計入;缺少 credentials 的驗證錯誤仍回 400。失敗鎖定保持獨立。
username 使用 trim+lowercase(Locale.ROOT),對齊目前 Account UsernameKey,不額外折疊 accent 或 Unicode。IP 取實際連線或容器可信代理接線解析的 remoteAddr,不直接信任任意 X-Forwarded-For。key 為長度前綴的 IP+username SHA-256 hex;不保存原始帳號/IP,hash 並非保密加密。
結果與 HTTP
正常允許:剩餘名額、下一名額時間、retryAfterSeconds=0。 拒絕:remaining=0、有 resetAt 與正數 retryAfterSeconds(向上取整、至少 1 秒)。 停用/故障:allowed=true、degraded=true、remaining=-1、resetAt=null、retryAfterSeconds=0,代表配額未知,仍需認證與 SQL 檢查。
兩個參考 AuthController 投影 429、Retry-After、X-RateLimit-Limit、X-RateLimit-Remaining、X-RateLimit-Reset(ISO Instant);ProblemDetail 帶 errorCode=rate-limit-exceeded、retryAfter 秒數。jar 不引入 HTTP 形狀。
X-RateLimit-* 是應用自訂 header,不宣稱標準 RateLimit 相容性。Reset 採帶 Z/offset 的 ISO Instant,對齊專案既有 Instant wire 表示且可直接辨認絕對可用時間;不採 epoch/相對秒數, 避免未明示單位。client 必須依此契約解析;重試等待以 Retry-After 數值秒數為準。此選擇已隨 方案 A 接受,HTTP adapter 由應用擁有,不寫入框架 jar。
並行與部署
參考矩陣 forward-headers-strategy=none;代理後若無可信解析,remoteAddr 為代理 IP,配額 退化為共用該代理的每 username 五次/分鐘。任意密碼五次即可阻擋該帳號剩餘視窗;部署方必須 先驗證可信 ingress 清理 headers、容器解析與阻擋直連 backend,才能宣稱實際 IP 隔離。 不得直接信任任意 X-Forwarded-For;詳細前提見 auth/security 指南與兩參考 server Migration。
單一 limiter 同步讀/判定/寫,避免同一物件內併發超額;同用途 cache 只能有一個 owner。同步區塊含 backend 與事件發布,延遲取決於 adapter/listener,沒有無等待或 timeout 保證。
預設 Ehcache heap=10,000 keys、TTI=60 秒。淘汰、故障、停用及重啟可能遺失配額,屬單 JVM best-effort baseline,非全叢集硬上限。多節點須替換為具原子配額決策的 PasswordLoginRateLimiter;普通 get/put 換 Redis 不能保證分散式原子性。
既有 cache 修正與事件
LoginLockout 保留原建構器與 lockoutDuration 覆寫接點。只捕捉 cache.isEnabled/get/put/remove 的 RuntimeException;密碼、帳號旗標、互動資格、政策及 SQL 操作在該邊界之外。Error 不捕捉,cache 建構/名稱解析及非法容量等啟動錯誤不吞掉。
Spring bean 用 ApplicationEventPublisherAware 接入 AuditApplicationEvent:
- CACHE_UNAVAILABLE:本用途第一次觀察到故障。
- CACHE_RECOVERED:本用途所有已觀察失敗的操作都實際成功後。 資料僅 cacheName/component,principal=system、時間為 Instant;component 是 login-lockout 或 password-login-rate-limit。管理停用不算 backend 故障,也不清除已知故障。isEnabled 成功不能獨自宣告恢復;GET 成功不能清除已知 PUT/REMOVE 故障。
這是每用途、每實例的觀察,非共享 backend 的完整診斷;新操作仍可能觀察到另一故障。普通 listener 例外記安全警告、不改登入結果,事件不承諾送達。應用自行訂閱並快速交給既有通知能力,本輪不決定管理者名單或通知 outbox。
tokenBlacklist 執行中例外維持傳播,OIDC/signed-link fail-closed 不變;SQL Session 撤銷仍具權威性。不得宣稱任何 cache 全面故障時所有 auth 路徑都可用。
相容性與驗證
新 API 納入目標 alpha.33。LoginLockout 原先 cache RuntimeException 可能回 500,本次改為略過該 cache 保護,需明示此行為修正。原建構器不變,Spring 管理的匿名 subclass 自動接 publisher;非 Spring 建立者須顯式設定才有事件。
參考 server 使用本地複合建構驗證,本次框架發布不另外切其 CalVer。下游採用須升級含此變更的 artifact,合併 auth feature/AuthController 接線並保留自有修改;既有 alpha.27/alpha.32 無新 limiter。
驗證涵蓋滾動邊界、Retry-After、IP/帳號隔離、並行五次上限、故障/恢復與停用、讀成功不掩蓋寫故障、listener 與 Error、真實 DAO 密碼/帳號資格、兩參考 server HTTP/SQL 撤銷。框架 security/cache 指定測試 247 項成功;兩參考 server 的 ./gradlew -PuseLocalFramework=true test 合計 1,445 項成功,XML 另列 12 項條件跳過或停用占位。無 Docker 的真實 MySQL 與外部瀏覽器代理案例未執行;不沿用前次審計測試結果。修正與測試見 d1a15837;報表非同步審計等待修正另於 85abfb2e 提交。
候選方案與決策
| 方案 | 取捨 |
|---|---|
| A:獨立 limiter+本機滾動預設 | 本稿最小實作,不混用失敗鎖定 |
| B:重用 M2M boolean limiter | 缺 retry/reset,擴張既有契約影響 |
| C:只用 gateway | 可加防護,但需另接 Account username 配額 |
| D:通用策略平台及可靠通知 | 超出穩定修正範圍 |
選擇 A:接受本文的 API 欄位、滾動視窗、單機 best-effort 與遷移契約。backend/同步 listener 可增加延遲,事件不保證送達;配額可因停用、故障、淘汰或重啟遺失。密碼、帳號資格及 SQL Session 檢查仍須生效,不擴張為通用 Cache 降級平台。
Decision Log
| 日期 | 事件 | 結果 |
|---|---|---|
| 2026-10-11 | 下游回報並重現兩項缺口 | proposed r1,保存現況失敗證據 |
| 2026-10-11 | 使用者確認框架修正流程 | bug fix 直接修正/測試/peer review,公開 API 另聚焦審議 |
| 2026-10-11 | r2 與最小工作區草稿 | 沿用事件、不擴張 blacklist/M2M;RFC 尚未 accepted |
| 2026-10-11 | r3:R1 人審回應 | 共用測試夾具、代理前提、header 選擇與公開型別範圍;人類裁決尚待記錄 |
| 2026-10-11 | 實作人審通過並收尾 | 第 2 輪 approve;R2-01 minor 完成處置,維護者明示依既有通過結論 close;保留結論於指南、Changelog 與提交說明 |
| 2026-10-11 | 維護者正式接受並授權發布 | 接受方案 A,status=accepted;decided_by=框架維護者(使用者);target_version=4.0.0-alpha.33;包含本文公開契約與限制 |
| 2026-10-11 | 發布與套件核對 | Maven releases 已發布 4.0.0-alpha.33;遠端 JAR 與本機產物 SHA-256 一致,POM 版號及 limiter/非同步審計類別核對通過;完整框架驗證合計 1,710 項成功,58 項跳過,包含已通過的 27 項隔離測試。發布前 audit 測試的 BUSY 前置修正經使用者人審通過,結論與 10 次獨立重跑驗證記於 45d79f7c |