跳至主要内容

ADR-009: 認證授權架構——雙模式資源伺服器

ADR 編號: 009 狀態: 已接受 (Accepted) 決策日期: 2026-06-24 決策者: Development Team 取代: 無 被取代: 無


摘要

app-server 採「雙模式資源伺服器」架構:以單一資源伺服器(Resource Server)核心同時承載「對內 UI 人類操作」與「對外契約被授權系統(M2M)」兩種 surface;發 token 端可在 standalone(自簽)federated(外部企業 IdP) 兩模式間以設定切換,共用同一套 JWT 驗證與 @PreAuthorize 授權。框架明確不經營完整授權伺服器(AS),強客戶端認證、MFA、SAML 一律委派企業 IdP。


背景 (Context)

問題陳述

app-server 的參考實作原本針對「有前端模組、人類操作」的情境,但實際也會有第三方系統以服務帳號(service account)連接呼叫 API 的情境(對外契約 surface,見方法論「兩條軌」與 ADR-003 Headless 軌)。現況已支援兩者——以單一 Account 實體 + serviceAccount 旗標區分人類與機器主體,並提供兩條發 token 路徑:

  • POST /api/v1/auth/login:人類登入(username/password → access + refresh + sessionId,服務帳號被擋)
  • POST /api/v1/auth/token:M2M client_credentials(client_id/client_secret → 短效 JWT,不發 refresh)

兩者落到同一個資源伺服器、同一套 Role/Authority 授權。但整套是手刻的(自訂 filter chain、JsonAuthenticationEntryPointJwtTokenProvider 自簽),未走 Spring 標準 oauth2ResourceServer。手刻的動機是標準元件未提供的兩個功能:token 黑名單(登出/撤銷)登入鎖定

需要決定的問題:

  1. 對外暴露的 OAuth2 scope 詞彙與內部 Authority 詞彙的關係
  2. 協定面錯誤格式(RFC 6749/6750)與應用面錯誤格式(RFC 7807)的分界
  3. M2M 客戶端認證強度(shared secret vs private_key_jwt / mTLS)
  4. 維持手刻 stack、自建完整 AS、還是當資源伺服器消費外部 IdP

限制條件

  • 企業客戶 IdP:框架支援的應用程式類型會有企業客戶帶自己的 IdP(Keycloak / Okta,甚至 SAML)需要連接 → 「當資源伺服器消費外部 IdP token」必須是一級支援的模式
  • 無未知第三方:對外 API 只有被授權客戶連接,不會有未知第三方以標準 OAuth2 library 接入 → 發 token 端不需要標準完整(不需公開 discovery / 嚴格 RFC 6749)
  • 不經營 AS:「經營認證伺服器」不在框架要支援的應用程式類型內 → 不把力氣投資在把框架養成完整 Spring Authorization Server
  • 框架定位:機制盡量上提到框架層讓應用快速開發;以 app-server 為範本 scaffold 亦可接受,兩者取彈性平衡
  • 現有資產SecurityConfig 已有 JwtDecoder + OAuthJwtAuthenticationProvider(驗證外部簽發 token 的接縫已存在);TenantContext.configureWithDefaults() 已支援從 JWT / OAuth2 claim 解析租戶
  • 授權粒度:既有端點以 @PreAuthorize("hasAuthority('...')") 強制,授權單位是細粒度 Authority(非角色)

假設前提

  • M2M 消費者皆為已知、可文件化的被授權客戶
  • 黑名單(即時撤銷)、登入鎖定屬 standalone 模式關注;federated 模式的撤銷與 MFA 由 IdP 承擔
  • 框架對「資料怎麼隔離/租戶從何而來」維持中性,tenant claim 解析兩模式共用(見 ADR-001)

考量的方案 (Options Considered)

方案 A: 維持全手刻 AS + RS,漸進補強

說明: 保留現有自訂 stack,手動補上 RFC 6749 token 錯誤格式、RFC 6750 bearer 挑戰,必要時自刻 private_key_jwt

優點:

  • ✅ 零遷移,保留現有黑名單 / 鎖定功能
  • ✅ 團隊熟悉、立即可動

缺點:

  • ❌ 持續重造標準輪子(錯誤格式、scope、private_key_jwt、JWKS…),每一項都是把協定/密碼學弄錯的機會
  • ❌ 無法滿足企業客戶 IdP 連接的常態需求
  • ❌ 自刻 private_key_jwt/mTLS 高風險(重放、audience、金鑰輪替)

評分: 2/5


方案 B: 自建完整 Spring Authorization Server

說明: 把 app-server 做成標準完整的授權伺服器(discovery、JWKS、標準 token 端點、private_key_jwt、mTLS 全配齊)。

優點:

  • ✅ 標準完整,第三方標準 library 即插即用
  • ✅ 強客戶端認證為設定而非自刻

缺點:

  • ❌ 違反「框架不經營 AS」的定位
  • ❌ 對「無未知第三方」的實際需求過度投資
  • ❌ 仍無法天然滿足「連企業既有 IdP」(你變成另一個 AS,而非消費客戶的 AS)

評分: 2/5


方案 C: 雙模式資源伺服器(採用方案)

說明: 以單一資源伺服器核心(標準 Spring oauth2ResourceServer().jwt() 驗證 + @PreAuthorize 授權 + TenantContext 解租戶)為兩模式共用基礎;發 token 來源靠設定切換:

  • standalone(自簽):無企業 IdP 的應用沿用現有輕量自簽(login + client_credentials + 黑名單 + 鎖定),保持輕量、不鍍金成完整 AS
  • federated(外部 IdP):有企業 IdP 的客戶讓 app-server 當純資源伺服器,驗證 IdP(Keycloak/Okta,OIDC)簽發的 token,人類與 M2M 皆在 IdP 認證

模式差異只在「token 來源」(本地金鑰 vs IdP issuer-uri/JWKS),不分叉業務碼——JwtDecoder + OAuthJwtAuthenticationProvider 就是現成接縫。

優點:

  • ✅ 直接滿足企業客戶 IdP 連接(federated)
  • ✅ 標準 RS 免費取得 RFC 6750 bearer 挑戰與 scope 對映
  • ✅ 強認證 / MFA / SAML 複雜度外包給企業 IdP,框架不自建
  • ✅ 同一套 @PreAuthorize 兩模式共用;機制上框架、靠設定切換,應用快速開發
  • ✅ 不過度投資(standalone 維持輕量,符合「不經營 AS」與「無未知第三方」)

缺點:

  • ❌ standalone 自簽非標準完整 AS(無 discovery / 嚴格 RFC 6749)——但對已知客戶可接受
  • ❌ 需維護與測試兩條設定路徑

評分: 5/5


決策 (Decision)

選擇方案: C(雙模式資源伺服器)

核心理由:

  1. 直接命中企業需求:federated 模式以純資源伺服器消費企業 IdP token,且驗證接縫(JwtDecoder/OAuthJwtAuthenticationProvider)已存在。
  2. 不過度投資:standalone 維持輕量自簽,不鍍金成完整 AS——符合「框架不經營 AS」與「對外只有已知客戶」。
  3. 複雜度外包private_key_jwt/mTLS/MFA/SAML 等高風險機制由企業 IdP 承擔,框架不自刻。
  4. 機制上框架、政策留範本:雙模式驗證機制進框架 jar、靠設定切換;Account/Authority 詞彙/ServiceAccountController 等政策與實體留 app-server reference impl 可 retarget——達成框架層 vs scaffold 的彈性平衡。

權衡分析 (Trade-offs)

我們獲得什麼 (Gains)

  • ✅ 企業 IdP 即插即用(federated 設定即可)
  • ✅ 標準 RS 免費附帶 RFC 6750 bearer 挑戰 + scope→authority 對映
  • ✅ 強客戶端認證 / MFA / SAML 零自建
  • ✅ 兩模式共用單一授權核心,降低分歧

我們放棄什麼 (Losses)

  • ❌ standalone 自簽不是標準完整 AS(無 discovery、不嚴格 RFC 6749)
  • ❌ federated 的即時撤銷依賴 IdP(或短 token TTL + introspection)

風險與緩解措施 (Risks & Mitigations)

風險嚴重性機率緩解措施
自簽 token 與 IdP token 的 claim 形狀分歧統一的 authority/scope 轉換器 + TenantContext 統一解 tenant claim;兩模式對映到同一套 resource:action
兩模式設定誤用(同時配本地金鑰與 IdP issuer)啟動時驗證兩者互斥;設定文件化
誤以為 app-server 要直接支援 SAML明定 SAML 經 IdP/broker 轉 OIDC,app-server 只驗 JWT
federated 模式撤銷不即時短 token TTL;高敏感場景採 opaque token + introspection

影響 (Consequences)

正面影響

  • ➕ 企業客戶可用既有 IdP 聯邦登入與 M2M
  • ➕ 協定標準合規(bearer 挑戰、scope)多為免費附帶
  • ➕ 高風險認證機制外包,縮小框架自有攻擊面

負面影響

  • ➖ standalone 與 federated 兩條設定路徑需維護與測試
  • ➖ reference impl 需同時示範兩模式

中性影響

  • 🔸 既有手刻黑名單 / 鎖定改定位為 standalone 模式關注點(保留,不廢除)
  • 🔸 既有 AuthorityR/W/X/D 模型收斂為 resource:action(見實作指南)

實作指南 (Implementation Guidelines)

必須遵守的規則

  1. 單一資源伺服器核心:採 Spring oauth2ResourceServer().jwt() 為唯一驗證/授權核心;模式差異只在 token 來源(本地金鑰 vs IdP issuer-uri/JWKS),靠設定切換、不分叉業務碼。

  2. 權限詞彙收斂為 resource:action(點 1)

    • 端到端單一詞彙——@PreAuthorize、JWT claim、IdP scope 設定使用同一套字串(如 order:readproduct:write)。理由:federated 模式下授權資訊來自 IdP claim,同一套 @PreAuthorize 必須同時吃自簽 token 與 IdP token。
    • 單一事實來源在後端權限常數;前端 mock 的權限映射為衍生鏡像(現已漂移,收斂時一併校正)。
    • :execute(狀態變更,原 X)為非標準 OAuth2 動詞——內部保留;是否對外暴露由授予政策決定(不暴露就不發該 scope),不為它改詞彙。
    • 不另立與 Authority 平行的 Scope 系統;「scope」即「token 攜帶 authority 的子集」。內外粒度若日後真分歧,再引入邊界映射層(YAGNI)。
  3. 錯誤格式分界(點 2)——協定面遵循 OAuth2 RFC、應用面用 RFC 7807:

    • token 端點(standalone /auth/token):SHOULD 回 RFC 6749 §5.2 格式(頂層 {"error","error_description"})。因客戶已知,列為 SHOULD 而非 MUST;federated 模式由 IdP 負責。
    • 受保護資源被拒:MUST 帶 RFC 6750 WWW-Authenticate: Bearer error="invalid_token"/"insufficient_scope" 標頭(隨標準 RS 免費);body 維持 RFC 7807
    • 人類 login/refresh/logout + 業務 4xx:RFC 7807 / 9457 ProblemDetail(不變)。
  4. 客戶端認證(點 3)

    • standalone:維持 shared secret(已授權客戶足夠),配 bcrypt 雜湊、輪替、短效 token、一次性顯示。
    • 強認證private_key_jwt / mTLS)、MFASAML:一律委派企業 IdP(federated),框架不自建
    • 認證方式為 per-service-account 屬性,預設 shared secret。
  5. SAML 經 broker:app-server 永不直接講 SAML;企業 SAML 由 IdP/broker(如 Keycloak)轉 OIDC,app-server 只驗 JWT。

  6. 黑名單 / 鎖定定位:黑名單 filter(撤銷)置於驗證前、屬 standalone 關注;鎖定屬登入/發 token 路徑。federated 模式即時撤銷與 MFA 由 IdP 承擔(或短 TTL + introspection)。

建議的最佳實踐

  1. tenant claim 解析統一走 TenantContext(兩模式一致;federated 從 IdP claim 對映,見 ADR-001)。
  2. federated 高敏感場景可選 opaque token + introspection(RFC 7662)取得原生即時撤銷。
  3. authority/scope 轉換器集中一處,兩模式共用,避免 claim 對映散落。

檢查清單

  • RS 核心採 Spring oauth2ResourceServer().jwt(),模式靠設定(本地金鑰 vs IdP issuer)
  • 權限詞彙為單一 resource:action,SoT 在後端,前端 mock 為衍生鏡像
  • 受保護資源回應帶 RFC 6750 WWW-Authenticate 挑戰標頭
  • standalone client 用 shared secret + bcrypt + 輪替 + 短效 token
  • 強認證 / MFA / SAML 場景走 federated(IdP),未在 app-server 自刻
  • SAML 不入 app-server(經 IdP broker 轉 OIDC)
  • 兩模式設定互斥於啟動時驗證

參考實作 (Reference Implementation)

app-server 的 AccountAuthority 詞彙、ServiceAccountController/ServiceAccountService、seed 帳號與角色定義屬 @reference-surface(見方法論 m-reference-code.md)——scaffold 後依專案領域 retarget(花店領域的 order/product/customer 換成目標領域)。reference impl 應同時示範 standalonefederated 兩模式的設定範例,作為下游應用的起點。

既有 Authority{RESOURCE}_{R|W|X|D} 模型(Read/Write/Execute/Delete),尚未被任何下游採用;本 ADR 將其收斂為 resource:action 形式(R→readW→writeX→executeD→delete),趁無採用者時建立單一詞彙。


相關文檔 (References)

內部文檔

  • OAuth2 使用指南:../guides/auth/oauth2.md
  • Bearer Token 認證:../guides/auth/bearer-token-authentication.md
  • Spring Security 配置:../guides/auth/security.md
  • Tenant 解析鏈:../guides/auth/tenant.md
  • Authority 權限模型:../guides/design/authority-model.md

外部資源

  • RFC 6749 §5.2(OAuth2 token 端點錯誤回應)
  • RFC 6750(Bearer Token 使用 / WWW-Authenticate 挑戰)
  • RFC 7523(JWT client authentication / private_key_jwt
  • RFC 8705(mTLS client authentication 與 sender-constrained token)
  • RFC 7662(Token Introspection)
  • RFC 9457(Problem Details,RFC 7807 之後繼)

相關 ADR

  • ADR-001: 多租戶數據隔離策略(tenant claim 解析共用):./001-multi-tenant-data-isolation.md
  • 方法論 ADR-003: Headless 契約驅動 API 軌(對外契約 surface 的軌道紀律):../../docs-methodology/decision-records/003-headless-contract-driven-api-track.md

變更歷史 (Change Log)

日期變更內容變更者
2026-06-24初版Development Team

文檔維護者: Development Team + AI Assistant 最後審閱: 2026-06-24