ADR-024: capability/feature/參考實作三層定位——feature 預期修改面限縮為組態接合,auth 資源實作降級業務層
ADR 編號: 024 狀態: 已接受 (Accepted) 決策日期: 2026-07-22 決策者: Development Team 取代: ADR-018 決策一、六(
Accountseam 模型與 catalog 宣告——帳號模型整域離開 feature 後,「canonical base + seam 具體類」二分不再是 feature 內的邊界,而是業務層自有碼的內部結構) 被取代: ADR-026 取代「Feature 只能含 組態接合」及「具體端點一律屬 reference implementation」的過度推論;auth 資源實作降級、 seam 收斂與低 drift 原則仍有效
2026-07-25 修訂提示:本 ADR 的「不得存在預設需要修改的非組態檔」不等於 「Feature 只能存在組態檔」。只依賴 capability、無合理 retarget 需求的固定 Actuator/health/ system handshake adapter 是零 drift core,依 ADR-026 留在 Feature。
摘要
確立 capability/feature/參考實作的三層定位原則:feature 存在的意義是低 drift 的同步管道,其預期修改面限縮為組態接合(*Config/*Properties/config fragment);凡預期會被消費端修改的內容都不屬 feature、屬參考實作(業務層、@reference-surface 生命週期)。據此把 auth 的資源實作(帳號模型、HTTP 殼、角色詞彙、seed)整域降級業務層,feature/auth 縮為純組裝 seam。前置:六個 feature 對帳號模型的 hard-import 須先契約化。
背景 (Context)
三層定位的張力:允許大量 drift 的 feature,與參考實作無差別
ADR-014 確立了 capability↔core slice 對偶,ADR-022/ADR-023 分別把 admin 面降級業務層、把登入引擎上收 jar。走完這兩步,一個此前隱含的定位問題浮上檯面:
feature 與參考實作的差別到底是什麼?
feature 的存在理由只有一個:同步管道——canonical 檔零分歧下行,框架修正自動到達 fleet。這個管道的價值與 drift 成反比:seam(消費端會改的檔)越多,feature 能同步的面越小;當一個 feature 的 seam 涵蓋 wire 殼、實體結構、專案詞彙、seed 資料時,它實質上就是穿著 feature 外衣的參考實作——而「單純縫合框架能力、允許大量 drift」這件事,參考實作本來就做得到,不需要 feature 機制的任何一部分(catalog、sync、provision、drift 分類)來服務它。
auth 正是這個病的最大標本。ADR-023 之後,feature/auth 的 seams 清單涵蓋:Account(實體結構)、AuthController(wire 殼)、RoleHierarchyService(角色詞彙)、SecurityConfig/StaticCorsConfigurationSource(組態接合)、Role.json/Account.json(seed)——只有組態接合那兩個是「縫合」,其餘全是參考實作。引擎上收後,slice 剩餘的 canonical(AccountRepository、AccountDetailsService、wire DTO)也被 ADR-023 自己的表格承認為「參考接線」性質。
紀律四已經給過答案,只是 auth 未跟上
21-feature-surface.md 紀律四:「feature 出能力與接點,資源的實作歸消費端;參考實作住業務層,不住 feature/」。mail 已走完資源分離:能力上收 jar,feature 組裝預設 bean;
MailSetting、CRUD 與 mail_setting:* 授權住附屬 mail reference domain。依 ADR-026
後續修訂,固定的 Actuator endpoint、health 與 mail:ops 授權屬零 drift Feature core,
不隨 MailSetting 降級。對 auth 資源實作而言,判定仍成立——FEDERATED 部署不曝露本地
login、身分在 IdP,帳號模型與 HTTP 殼整組可以不要且預期依專案重塑。
阻礙:帳號模型是六個 feature 的 import hub
實測掃描(2026-07-22),以下 feature hard-import feature/auth 的帳號模型型別(Account/AccountRepository/RoleHierarchyService/UserResponse/TokenBlacklistService):
| Feature | 檔案 | 依賴什麼 |
|---|---|---|
delegation | DelegationService | 帳號/角色查詢(合併授權人權限) |
impersonation | ImpersonationService、ImpersonationPolicy、AntiEscalation*、ConsentAware*、ImpersonationConsentService(5 檔) | 帳號/角色查詢、TokenBlacklistService |
service-account | ClientTokenController、ServiceAccountManagementService(原 ServiceAccountService) | 服務帳號載入與驗證 |
signed-link | SignedLinkAppService | email 查找帳號 |
notification(inapp) | InAppNotificationController | 當前使用者身分解析 |
今天這些是合法的 feature→feature 邊(requires: auth);帳號模型搬到業務層後它們變成 feature→業務邊,違反 ADR-014 中性不變量(ArchUnit 擋)、並使這些 feature 無法單獨 provision。契約化這些邊是本 ADR 的硬前置。連帶暴露的既有壞味道:UserResponse 這個 wire DTO 被跨 feature 共用。
限制條件
- ADR-014 中性不變量:feature 碼不得 import 業務層(ArchUnit 強制)。
- UI 軌專案需要可運行的本地登入面——降級不得使 scaffold 交付不可登入的系統。
- wire 契約(
/api/v1/auth/*)與行為零變化。 - 時機同 ADR-023:SNAPSHOT、fleet 小,搬遷成本最低的窗口。
假設前提
- ADR-023 已落地:登入引擎在 jar,slice 對引擎無編譯依賴。
- 安全承重面的同步已由 Maven 管道承擔;slice 剩餘 canonical 的同步價值低(帳號模型本就 per-project)。
考量的方案 (Options Considered)
方案 A: 維持現狀(寬 seam 的 feature)
說明: auth 保持 ADR-023 後的形狀:引擎在 jar、slice 帶寬 seam 集(實體、殼、詞彙、seed)。
優點:
- ✅ 零遷移成本
- ✅
AccountBase/AccountDetailsService保留 canonical 同步
缺點:
- ❌ feature 與參考實作的定位持續模糊——「什麼該是 seam」無判準,seam 集會繼續膨脹(historical:
Account→AuthController→RoleHierarchyService→ seed,一路加) - ❌ sync/provision/drift 分類持續為一堆「下游本來就會改」的檔運轉,報告噪音掩蓋真訊號
- ❌ 紀律四對 mail 成立、對 auth 不成立——同一 workspace 兩套標準
評分: 2
方案 B: 只搬 auth、不立原則
說明: 把 auth 資源實作降級業務層(比照 ADR-022),但不確立通用定位原則。
優點:
- ✅ 解掉最大標本
缺點:
- ❌ 「seam 該多寬」仍無判準——下一個 feature 還會長出寬 seam,每次都要重新吵一遍 auth 這場架
- ❌ 既有其他 feature 的 seam 集無稽核依據
評分: 3
方案 C: 確立三層定位原則 + auth 整域降級(採用)
說明: 先立原則(決策一),auth 作為第一個套用案例(決策二~四),既有 feature 的 seam 集依原則盤點(決策五)。
優點:
- ✅ 定位一次說清楚:capability=能力、feature=低 drift 縫合、參考實作=預期重塑的起點
- ✅ seam 有了機械判準(
*Config/*Properties),膨脹被結構性擋住 - ✅ auth 完成 ADR-022→023→024 的三部曲:admin 面、引擎、資源實作各歸其位
缺點:
- ❌ 前置(六 feature 契約化)工作量大,須分期
- ❌
AccountBase等剩餘 canonical 的同步權喪失
評分: 5
決策 (Decision)
採方案 C,五個決策。
決策一:三層定位原則(本 ADR 的核心貢獻)
| 層 | 定位 | 住哪 | drift 期望 | 同步管道 |
|---|---|---|---|---|
| capability | 能力本體+SPI 邊界 | appfuse-server jar | 零(消費端碰不到) | Maven 版本升級 |
| feature | capability 無法在 jar 內完成的組合:bean 組裝、config 匯入、貢獻點接線 | feature/{id}/(slice) | 趨零——預期修改面限縮為組態接合 | /feature sync(零分歧覆蓋) |
| 參考實作 | 預期依專案重塑的資源實作:實體、殼、詞彙、seed | 業務層 | 必然(這就是它的生命週期) | 無——@reference-surface,retarget 或 prune |
不變量:feature 內除
*Config/*Properties(含 config fragmentfeature-{id}.yml)外,不得存在「預設需要修改」的檔。
- 判定句:「這個檔預期會被消費端修改嗎?」預期會、且不是組態接合 → 它不屬 feature,屬參考實作(業務層)。
- seam 的語意收斂:seam 從「feature 內任何消費端擁有的檔」收斂為「組態接合點」。組態接合天生 per-project(jar 不做 autoconfiguration 的直接後果),是 feature 唯一合法的 drift 面。
- feature 內 drift 是訊號,不是常態:非組態檔出現持續 drift = 設計錯位,出口只有兩個——變異點反覆出現 → 抽 SPI 上收 capability(ADR-023 路徑);整個資源的實作分歧 → 降級參考實作(ADR-022 路徑)。沒有「把它列成 seam 繼續住在 feature」這個第三出口。
- 為什麼:feature 的存在意義是同步管道;允許大量 drift 的「feature」與參考實作提供的東西完全相同,卻多付 catalog/sync/drift 分類的機制成本。低 drift 不是 feature 的品質指標,是它的定義。
- 對 seam 設計的推論:既然 seam 只剩組態接合,它就必須從最佳實踐出發設計——組態接合的形狀(裝哪些 bean、接哪些 SPI)本身應當是 drift 機率極低的 canonical 知識,消費端改的是值與選擇,不是形狀。
決策二:auth 資源實作整域降級業務層
搬遷清單(app-server 為 layer-first,業務域名 auth,與既有 authadmin 域並列):
現位置(feature/auth/) | 去處(業務層) | 性質 |
|---|---|---|
Account、AccountBase、Role、Authority、RoleType | entity/auth/ | 帳號模型(AccountBase 自此為業務層自有碼——canonical/seam 二分退役,見「與 ADR-018」) |
AccountRepository、RoleRepository、AuthorityRepository | repository/auth/ | 持久化 |
AccountDetailsService、AccountUserDetails、RoleHierarchyService、TokenBlacklistService、AuthSeedContributor | service/auth/ | adapter 與詞彙服務(AccountDetailsService 續任 jar AuthPrincipalLookup/UserDetailsService 實作) |
AuthController、LoginRequest、LoginResponse、RefreshTokenResponse、UserResponse | controller/auth/ | HTTP 殼與 wire DTO(UserResponse 自此不得再被 feature import——見決策三) |
data/auth/Role.json、Account.json | 隨 AuthSeedContributor 走(seed 路徑不變或併整) | 專案詞彙 seed |
feature/auth 殘餘(純組裝,符合決策一不變量):SecurityConfig、StaticCorsConfigurationSource、H2ConsoleSecurityConfig(皆組態接合 seam)+ config fragment。catalog 的 seams 縮減為此集合;kind: required 不變(每個專案都要組 filter chain)。
決策三:前置——六條 feature→帳號模型的邊契約化(硬前置,分期)
依 ADR-023 的同一手法逐 feature 收斂,全部斷完才能搬(否則 ArchUnit 中性檢查擋)。最難的一條邊(delegation/impersonation 的「查他人」)已於 2026-07-22 spike 定稿,結論:一個新 surface 就夠,且不需要角色階層 SPI(展開歸 wire 層,見下)。
契約定稿:PrincipalDirectory/PrincipalView(jar,io.leandev.appfuse.security.directory)
從 delegation/impersonation/signed-link 的實際用法逐行萃取(非臆測需求)的唯讀主體目錄視圖:
/// 主體目錄視圖(唯讀)——feature 對「他人」的全部合法認知
public interface PrincipalView {
String username(); // 身分識別子
default String id() { return username(); } // 穩定識別(無獨立 id 的身分模型退回 username)
String displayName(); // 顯示名(實作以 username 退回,不回 null)
default Optional<String> tenantId() { return Optional.empty(); } // ownership 專案恆 empty
default boolean interactiveLoginAllowed() { return true; } // 同 AuthPrincipal 語意
Optional<String> primaryRoleName(); // 主角色裸名
List<String> roleNames(); // 直接指派的角色裸名(順序=帳號角色順序)
List<String> authorityNames(); // 攤平的權限名
}
public interface PrincipalDirectory {
Optional<? extends PrincipalView> findByUsername(String username);
Optional<? extends PrincipalView> findById(String id); // 簽章連結等以不可變 id 為 subject 的兌換載入(ADR-007)
List<? extends PrincipalView> findByEmail(String email); // 重複容忍(ADR-018 決策三:List、不選第一筆)
List<? extends PrincipalView> findAllInteractive(); // 候選池原料;scope 過濾(同租戶/跨租戶)歸 feature
}
實作期修訂(2026-07-23):①補
findById(signed-link 的連結 subject=不可變帳號 id,兌換須精準載入); ②啟用 email 的 fallback 條款——PrincipalView增選用default Optional<String> email()(預設 empty)。 consent 通知須以 email 組收件人,改走RecipientResolver路徑會迫使遞送流程改變+新增 app 端 resolver, 成本高於一個預設 empty 的選用成員;「email 是通道不是身分」的立場由 javadoc 與預設值承載。
刻意排除的成員(每個排除都是少一分偏見):
| 排除 | 理由 |
|---|---|
email(聯絡通道) | ADR-018:email 是通道不是身分。實作期已採 fallback 條款(見上修訂):降格為選用成員(default Optional<String> email(),預設 empty)而非契約必要成員——無 email 概念的身分模型零負擔,消費端對 empty 須退 RecipientResolver 等通道解析 |
| 角色階層展開(可達角色、主角色置首) | 那是 wire 契約(前端 guard 吃展開陣列、roles[0]=主角色),不是目錄事實。展開留在業務層 wire 組裝(RoleHierarchyService——它本身是專案角色詞彙,本就要隨決策二搬業務層)。連帶語意修訂(已落地):jar ActingLogin.roles() 由「合併展開角色」改為「合併後的直接角色」,展開由業務層 wire 面(AuthController.buildUser)執行 |
| 密碼/憑證 | 認證走 AuthenticationManager,目錄永不曝露 secret |
逐邊處置(定稿)
| 邊 | 處置 |
|---|---|
impersonation → TokenBlacklistService | 改直接吃 jar TokenBlacklistStore(純機械,ADR-023 引擎已示範) |
notification(inapp)→ 當前使用者 | 改吃 Spring Security context/AuthPrincipal,不觸 entity |
signed-link → email 查找 | PrincipalDirectory.findByEmail(重複容忍語意隨行) |
delegation → 帳號/角色查詢 | contribute() 的合併基底改 AuthPrincipalLookup(grantee 現時 authorities)+ PrincipalDirectory(grantor 的 roleNames/authorityNames);候選池 findAllInteractive()+tenantId() 過濾;同租戶/服務帳號守衛用 tenantId()/interactiveLoginAllowed() |
impersonation → 帳號查詢與政策 | 同上走 directory;ImpersonationPolicy 簽章改 allowed(PrincipalView actor, PrincipalView target)(比照 ADR-023 對 LoginPolicy 的處理,SPI 不得洩漏 entity——ADR-014 規範一) |
(橫向)UserResponse/LoginResponse 跨 feature 共用 | session wire 組裝一律住業務層:impersonate/exit/exchange 回應的是「登入形狀」payload,屬 app 的 session wire 面——feature 曝露引擎級結果(ActingSession/ExchangedSession)、不 import 業務 wire DTO。實際落點(2026-07-23):wire 殼按 feature 分域住 controller/acting/(ImpersonationController)與 controller/signedlink/(SignedLinkController)——不併入 controller/auth/,使 prune 域集清晰。delegation 的 ActingLogin 已是引擎級(jar record),僅語意修訂(見上) |
service-account → 服務帳號載入與驗證 | 本 ADR 定稿(Phase 1d,2026-07-22;token 端點的暫時落點其後由 ADR-025 取代):不開帳號 provisioning SPI。管理面(現名 ServiceAccountManagementService/Controller/authorities——建帳號、輪替 secret、啟停用)是寫入型資源實作,降級業務層;M2M 發放操作後續上收 capability,ClientTokenController 移至業務層 controller/auth/,service-account feature 移除 |
2026-07-24 生命週期修訂:上表「資源實作降級 reference implementation」仍成立; 「Feature 移除」則由 ADR-025 的修訂 取代。Catalog
referenceDomains現可讓 optional Feature 附帶可客製資源實作而不把它誤當 core, 因此service-account恢復為 optional;controller/service/entity/repository 仍屬serviceaccountreference domain,M2M/API-key 固定機制仍在 capability。
PrincipalDirectory 的參考實作住業務層(service/auth/ 以 AccountRepository 實作),feature 只認介面。每斷一條邊該 feature 即恢復可單獨 provision;全斷完後執行決策二的搬遷。
決策四:搬遷物的分類與生命週期
- 業務殘餘、scaffold stamp
@reference-surface(比照 ADR-022):帳號治理模式本就 per-project,「retarget」的語意是「重塑成你的身分模型」而非改詞彙。 - prune 出口存在但有條件:FEDERATED/外部身分源部署可整域剝離(本地登入面不存在);UI 軌專案不可 prune(登入是必要面)——
/prune-reference對此域應以「專案是否有本地登入需求」提示,而非放行後建構失敗才發現。 - 不新增 keep-as-is 例外:對預設身分模型滿意的專案,其「retarget」可以極薄(確認即拔標記,由
/impl//domain-model認證),但確認這一步不可省——這是把「帳號模型是你的」明確化的儀式。
決策五:既有 feature 的 seam 盤點義務
catalog 內全部 feature 的 seams 依決策一不變量盤點一輪:非 *Config/*Properties/config fragment/的 seam 逐一判定——抽 SPI 上收、降級業務層、或(少數)論證留置的理由寫進 catalog 註記。盤點結果與處置另立 FU 追蹤,不阻塞本 ADR 的 auth 主線。
實作補充:複雜度優先序與端點所有權(2026-07-24)
後續 auth、service-account 與 API-key 重構把本 ADR 的判準再具體化:
- 機制的首要目標是降低 reference implementation 的複雜度,其次才是降低 Feature 的複雜度;固定且容易寫錯的技術複雜度可由 capability 承擔一次。
- 產品資源的 URL、HTTP method 與 public/authenticated policy 是 reference implementation 的對外契約;固定 application integration surface 則由 Feature 擁有(ADR-026 修訂)。
- Feature seam 可以是 reference implementation 寫入這些政策的組態接合點,但位址本身不因 住在 seam 檔中就成為 Feature contract,也不得進入 capability。
- 公開端點採 exact method + path;不以共用 prefix wildcard 讓未來端點自動匿名可及。
platform-info的設定遮蔽/來源追蹤/DB 檢查與版本缺值語意上收appfuse-servercapability;Feature 組裝ConfigurationDiagnostics、SystemInfo與 Actuator info contributor。依 ADR-026 修訂,固定的/actuator/configcheck、/api/v1/system/info及其授權亦屬 Feature core;該 feature 沒有 reference domain。與標準 health、 system-info 重疊且會反射完整 request headers/Authentication的/actuator/helo仍維持移除。
目前 app-server SecurityConfig 的 auth matcher 即屬 reference implementation 對 seam 的內容:
它精確列出 login、refresh、logout、M2M token、exchange 與 signed-link consume。
這些路徑是參考值,不是 /feature sync 應強加到下游的固定 API。
後果 (Consequences)
正面
- 三層定位一次說清:capability=能力(jar、Maven 同步)、feature=低 drift 組態縫合(sync 同步)、參考實作=預期重塑的起點(無同步、有標記)。之後每個「這該放哪」的爭論都有判定句可用。
- feature 機制回到它值得的形狀:sync 零分歧覆蓋的面=slice 全部非 seam 檔;drift 報告只剩真訊號。
- auth 三部曲完成:ADR-022(admin 面歸消費端)+ ADR-023(引擎歸框架)+本 ADR(資源實作歸消費端)——「引擎歸框架、資源歸消費端」的完整佈局。
- 帳號模型的所有權誠實化:
Account/AccountBase的「canonical+seam」二分(ADR-018 決策一)是「引擎住在 slice」時代的妥協;引擎走後,整個帳號模型歸消費端,空體紀律、FQN seam 引用等機制隨之退役,認知負擔下降。
負面/代價(誠實列出)
- 前置工作量大:六條邊的契約化各是一次 ADR-023 尺寸的設計(雖然手法已熟)。分期執行,週期內 auth 維持現狀。
- 剩餘 canonical 同步權喪失:
AccountBase(username 唯一性、憑證欄位)、AccountDetailsService(authority 組裝、啟停日期衍生旗標)的框架改良(如 FU-66)不再自動下行。緩解:安全承重面已在 jar;帳號模型的改良本就常是 per-project 需求。 - jar 契約面再擴大:決策三新增的 SPI 都是公開 API,受 CHANGELOG/deprecation cycle 約束——又一個「趁 SNAPSHOT」的理由,也是一個「契約要窄」的警鐘。
feature/auth變得很薄:殘餘只剩三個組態檔。這是 feature 該有的樣子(決策一),不是退化——但 catalog 上它會顯得「小」,需要這個 ADR 解釋為什麼小才是對的。
遷移步驟(分期)
- Phase 0(本 ADR 接受時):
21-feature-surface.md補決策一不變量(seam 語意收斂+判定句+兩出口);決策五的 seam 盤點開 FU。 - Phase 1(逐 feature 契約化,可並行):依決策三定稿逐邊實作(
PrincipalDirectory進 jar → 業務層參考實作 → feature 改接)→ ArchUnit 驗證該 feature 對feature.auth帳號型別 import 歸零;service-account邊於 1d 定稿其 SPI。 - Phase 2(搬遷):決策二整域搬遷(git mv + package 調整);
features.jsonseams 縮減;wire 契約零變化以AuthAdminIT等既有 IT 驗證。✅ 已完成(2026-07-23)——帳號模型→entity/auth+repository/auth、adapter 與詞彙服務→service/auth、wire 殼與 DTO→controller/auth;feature/auth殘餘SecurityConfig/StaticCorsConfigurationSource(seam)+H2ConsoleSecurityConfig(canonical),seams 縮至兩個組態檔;SecurityConfig改以 SpringUserDetailsService介面接 DAO provider(不 import 業務類);殘餘推導報表修正 layer-first 下「域名與 feature 同名」的誤過濾(auth/serviceaccount等現形為 stamp 目標)。 → 後續修訂(2026-07-23,FU-71 I4 閘門第一滴血):帳號模型降級使 ownership 組裝樹(skeleton+features)無法開機——SecurityConfig組裝引擎所需的UserDetailsService/AuthPrincipalLookup/PrincipalDirectory實作全在業務殘餘。解法為框架 catalog 頂層新宣告variantSharedDomains: ["auth", "authadmin"]:所有組裝樹都需要的降級參考起點隨 variant 下行(scaffold 照常 stamp@reference-surface);optional 的 acting 與 signed-link 改由各自referenceDomains附帶,只有選用 feature 時才出貨。判定句有兩個條件:①是降級參考起點、非來源領域業務(花店域不進);②隔離中性(auth delta=0 家族)——serviceaccount管理面 hard-import tenant feature、mail的MailSetting帶@TenantId血緣,皆不合格、不進共用域(其 capability 面不受影響:mail 無 provider 可運作、M2M token 端點隨 feature 下行)。variant-assembly.py依宣告自排除集扣除;AuthAdminIT於 ownership 樹全綠,實證 claim-based 租戶邊界政策(ADR-020 D-C)隔離中性。此修訂同時緩解 ADR-022 的負面條目「非 base scaffold 不帶帳號管理起點」——ownership scaffold 自此帶可登入的帳號模型與 authadmin 管理面參考起點。 - Phase 3(收尾):scaffold stamp 驗證(新 scaffold 對
{layer}/auth/打標);/prune-reference域集推導涵蓋本域+「本地登入需求」提示;ADR-018 標頭補決策一、六取代備註;guides 對齊。
相關
- ADR-014(capability↔core slice 對偶;決策一是其「slice 該有多薄」的補完)
- ADR-022(admin 面降級——本 ADR 決策二的直接前例與姊妹)
- ADR-023(引擎上收——本 ADR 的前置;決策三沿用其契約化手法)
- ADR-018(決策一、六由本 ADR 取代;決策三〔email 重複容忍〕語意隨 signed-link 契約沿用)
21-feature-surface.md紀律四(「參考實作住業務層」——決策一是其一般化:連 seam 都收斂為組態接合)m-reference-code.md(@reference-surface生命週期——決策四的分類依據)