固定期限 Session profile
概述
本頁適用已發布的 Server 4.0.0-alpha.39,與 Web 19.0.0-alpha.136 配對。opt-in 後,互動 Session 的固定截止、JWT Access 上限、即時 family 撤銷與 Browser fence 共用一份契約;未 opt-in 保留 sliding idle/remembered 組裝選項。共用的 credential 前置驗證、resolved-principal 簽章、sessionKind claim 與 store 的 policy-inclusive 到期判定也影響 legacy caller;須連同既有登入、Cookie、replay 與授權測試驗證。
目前參考應用未啟用此 profile。新增的兩種 SQL store 與 AccountSessionTransactions 提供採用接縫及測試證據;產品需完成全部 HTTP 入口、DTO、protocol gate、Cookie replacement 與前端協調器的遷移才啟用。正式 OTP/OIDC proof、owner/authority 政策、郵件與 UAT 不由本能力代替。
快速開始
由可信 Server 組裝建立並共用同一 SessionFamilyProfile、權威 RefreshSessionStore 與 Clock:
import io.leandev.appfuse.security.auth.SessionFamilyProfile;
import io.leandev.appfuse.security.auth.SessionPrincipalRegistry;
import io.leandev.appfuse.security.auth.DefaultAuthPrincipal;
var profile = new SessionFamilyProfile("interactive-v1", Duration.ofHours(12));
var principalContracts = new SessionPrincipalRegistry(Map.of(
DefaultAuthPrincipal.class, "Account UUID; application-scoped, immutable across username/email changes"));
LoginService 的新建構器依序接收既有完整依賴、Optional<SessionContextPolicy>、profile、principalContracts。default SessionOptions 必須 non-remembered,idle 覆蓋固定 lifetime;Access TTL 是正整秒。一般 binding 缺省時引擎自動補 profile 截止,既有 login/issue overload 也不能繞過。
在原 ResourceServerSecurity.Builder 呼叫 sessionFamilies(profile, store, clock),再 configure。JWT 認證後、authorization 前逐次查 authoritative family。JwtTokenProvider 的 Spring singleton 啟動驗證核對引擎與 filter 安裝;漏裝、profile 不同、store/Clock 不是共用實例,或 capabilities 不足都拒絕啟動。非 Spring 組裝須明示呼叫 afterSingletonsInstantiated() 完成相同驗證。
profile 查核 Bearer JWT:以 JwtAuthenticationToken 或 Authorization 的 Bearer scheme 識別來源,converter 須保留已驗章 Jwt principal;框架內建 standalone/federated converter 符合此契約。自訂 Bearer converter 若將已認證 principal 改成業務物件,family filter 回 500 session-validation-failed 並中止授權,避免靜默跳過權威查核。
可在同一 ResourceServerSecurity.Builder 同時啟用 sessionFamilies、apiKey 與 basicAuth。API key/Basic 的 AuthPrincipalAuthenticationToken 沿各自的逐請求認證、資格與 authorization policy,不進入 JWT family lookup,也不因缺少 Jwt principal 回 500。即使同時帶有效 API key,Bearer 認證失敗或 family 已撤銷仍拒絕,不能退回 API key 繞過;匿名請求仍交給原 authorization policy 判定。
principal registry 登記所有可發行的具體 AuthPrincipal type 與 immutable application account ID 契約。不能用 Email/tenant-local username。反射只查 subject 方法來源,不能證明資料不變;應用須以實際改名與撤銷測試證明。DefaultAuthPrincipal 與子類還須在建構時明示非空 subject;未設、null、空白均會在 mutation 前以組裝缺陷拒絕,不映成 401。
HTTP 發行與 Cookie replacement
HTTP 殼在 proof 消耗與任何副作用之前檢查 X-Appfuse-Session-Protocol: 1。缺少或不符回 409 session-client-upgrade-required,不寫 Cookie、不消耗 OTP。CORS 允許 protocol 與 X-Appfuse-Session-Family;SDK operation header 只屬 mock,正式 Server 不信任它授權。
產品密碼/OTP/OIDC 等 Cookie-backed 發行都傳 new SessionIssuanceContext(previousCookieCredential),沒有 Cookie 也明示 context(null)。使用新增的 login/issue overload;不要從 browser body 接受舊 family ID。引擎成功建立新 family 後撤銷 Cookie 證明的舊 family,其他裝置保持;新發行失敗必須 rollback,不能留下先撤舊或多出新 family。
本輪實作審閱補正持有證明:replacement 與 Cookie-only logout 都以結構合法 Cookie 內的高熵秘密 selector 定位 family(legacy 無 selector 時要求完整 credential hash 相符)。因此目前 verifier、已輪替的舊 verifier、同秘密 selector 但未見過的 verifier,在這兩種操作都證明持有同一 family;expectedFamilyId 只做 fence,公開 family UUID 本身不是證明。新 proof 完成且新 family 建立成功後才撤舊,失敗須 rollback。refresh 另要求 current verifier;帶同秘密 selector 的非目前 verifier 仍走 replay 拒絕/撤銷,不能取得新 Access。
應用 transaction 包住 proof 的最終 account 版本檢查、發行/replacement、loginService.describe(result) 與 wire 組裝,commit 成功後才寫 Cookie。describe 驗證自家 Access JWT、result、權威 family 的 ID/subject/tenant/actor/deadline,不由應用自行解碼 JWT 推期限。
wire 包含 accessToken、metadata.sessionId、metadata.absoluteSessionExpiresAt;expiresIn 由實際 accessExpiresAt 換算,不固定回原 JWT TTL。LoginResult/RefreshResult 的 record components 與既有建構器保留。新 profile 的 Cookie-backed controller 漏傳 issuance context 是組裝缺口;保留的非 HTTP overload 只保證共同期限 guard。
refresh、身分切換與 logout
| 入口 | 接線要求 |
|---|---|
| refreshSession(credential, expectedFamilyId) | known family 必帶 fence;僅乾淨 bootstrap 可 null。先 credential/replay、再 fence、再 profile/deadline、最後發行與 rotation |
| switchToImpersonation/exitImpersonation | 無新 proof,deadline 取來源與目標限制較早者;退出不重設 lifetime。目標 binding.policy 與 profile.policyId 相符,其他受信任限制放 reference/requiredActorAuthorities 與 eligibility policy |
| activateSession | 在同一交易檢查原 profile/deadline/principal、activatePending 及 refresh;任何拒絕 rollback,不延長 pending 原截止 |
| deprecated refresh(String) | profile 下拒絕 JWT Refresh,不能用 rollout fallback 繞過權威 store |
| logout(access, cookie, expectedFamilyId) | 已知 family fence 必填;Access、Cookie、expected 不一致回獨立 409。Cookie-only 與可定位的舊 generation 撤銷同一 family;commit 後再清 Cookie |
JWT exp 向下取整為秒,Access 期限是 min(now + Access TTL, hard deadline)。不足以產生有效 Access 時拒絕,refresh/rotation 不延長截止。profile 不支援 remembered 或比 deadline 更短的 idle;傳入不符明確拒絕。
HTTP 殼分類如下,不能用一個 AuthenticationException catch 涵蓋全部:
| 結果 | HTTP/Cookie |
|---|---|
| 有效 credential 的 family fence 不符 | 409 session-changed;保留 Cookie,不重試另一 family |
| refresh 無效/缺 deadline/錯 profile/到期 | 401 refresh-session-invalid;清相符目前 Cookie,要求新 proof |
| 資源的 family 不存在/撤銷/脈絡不符/到期 | 401 access-token-invalid |
| store 暫時不可用 | 503 authentication-service-unavailable;保留 expectation,不寫成功或清除 Cookie |
| 未知缺陷/能力宣告不實/principal 組裝錯誤 | 500;不偽裝成認證失效 |
Store 與帳號安全事件
新 profile 要求 FAMILY_LOOKUP、PRINCIPAL_REVOCATION、POLICY_REVOCATION。舊 adapter 預設空能力,新 lookup/principal revoke 預設 unsupported;舊 revokeByPolicy 的空結果只適用舊組裝。lookup 不使用 positive active cache 或延遲 replica,撤銷 commit 後的新授權立即拒絕。blacklist 只是提前拒絕。
principal revoke 以 immutable subject 撤銷 subject 或 actorSubject 相符的既有 family,不受目標 family 的 tenant 限制,不是匿名 HTTP API,也不是永久禁止新登入。帳號停權、密碼重設等事件還須結合 credential/eligibility 變更及最後發行版本檢查。
兩種參考 Server 的 AccountSessionTransactions 示範:在 proof 前 captureBeforeProof(accountId),驗證成功後以原 ProofVersion 呼叫 issue(proofs, issuance);subject/actor 都列入。adapter 依 UUID 排序取得 account row lock,強制重讀目前版本,檢查資格,再在同交易發行及 replacement。securityEvent(ids, mutation, reason) 沿同一鎖順序完成實際帳號變更與 principal revoke。呼叫端先完成本人/管理授權,且所有 account 寫入放在 adapter 取得鎖後的 mutation 內;不能先改 managed Account 再讓 refresh 覆蓋它。
先 commit 的發行由後續 revoke 涵蓋;先 commit 的安全事件使舊 proof 版本不能發行。兩個 type 都有真實 SQL 測試,包含 actor、實際 rename、並行 commit 順序與 replacement rollback。這份 adapter 不自動改寫各產品已存在的停權/密碼重設流程。
AccountSessionTransactions.securityEvent 的即時 Access 撤銷依賴 bounded family filter;它更新 SQL 並回傳 family IDs,沒有自動填入 legacy Access blacklist。未啟用 profile 的應用不能只採用此 adapter 就宣稱舊 Access 已即時失效,須沿原 LoginService 撤銷/blacklist orchestration 完成。
Host Mock
import io.leandev.appfuse.security.auth.mock.MockAccessSessionOptions;
import io.leandev.appfuse.security.auth.mock.MockAuthSessionController;
var nativeController = MockAuthSessionController.inMemoryWithAccessSessions(MockAccessSessionOptions.defaults());
僅在明示 app.msw.enabled=true 的 Mock host 註冊 controller。預設 factory 仍是 Refresh-only;新 factory 加入 /api/v1/auth/__mock-session/access-sessions 的 issue、refresh、inspect、revoke。使用同一 store 管 legacy/新 family,不互相升降級。Cookie 是 HttpOnly、SameSite=Lax、Session Cookie,path 包含 context-path,Secure 隨有效 HTTPS request。
若 Mock host 同時裝有正式 JWT resource chain,Native namespace 須使用只在 mock 模式存在的獨立 security chain;opaque Bearer 由 inspect 判定,不能先送正式 JWT decoder。只寫 permitAll 不會跳過 bearer decoding。正式產品不能因此放行其他 API,也不註冊 Mock controller。
Native 錯誤為 application/problem+json;相同 requestId 重放只回摘要、不重新發 Token/Cookie。claims 只接受 scalar,保留身分/期限 keys 不能覆寫。預設上限 900/43200 秒、去重 86400 秒是開發護欄,產品仍明示 TTL。完整 Browser 接線見 Web 指南。
API 與驗證參考
公開類別位於 io.leandev.appfuse.security.auth,Host 型別位於 auth.mock,resource 組裝位於 security.resourceserver。詳見 ADR-030 與配對 RFC。
框架定向 security/Native 測試、test-contracts/mock-access-sessions.json 的共同案例,以及 Web 三環境 Playwright 測試一起驗證。Browser HTTP fixture 使用記憶體 SPI;SQL persistence/交易證據另外由兩個參考 type 的 BoundedSessionPersistenceIT 與 RefreshSessionPersistenceStoreIT 提供,不能把 Browser fixture 冒稱產品資料庫或產品 UAT。
參考源碼使用本機新 API 時,以 -PuseLocalFramework=true 建置。配對 artifact 已發布;正式消費端先升級到上述確切版本,再採用 source changes。本次發布未修改參考應用既有 dependency pin 或啟用新 profile。