CR #201 » SCS-022_認證與授權程式設計規格.md
SCS-022 認證與授權(SSO/JWT)程式設計規格
| 項目 | 內容 |
|---|---|
| 文件編號 | SCS-022 |
| 版本 | 1.11(新增 §7.11 使用者權杖轉傳(Token Relay)——BEAM PMO 2026-09-21 17:10 核定採 B)⚠️ 1.10 版執行後未回填本欄(仍記 1.9),本次一併更正 |
| 狀態 | 已核定(規格先行·跨域)——依 BEAM PMO 2026-09-19 指示「接著依序完成 W-3 SSO」 |
| 對應需求 |
BRD-003:Authentication 21 項、SSO 1 項(皆對應 SAD-005 §3) |
| 技術棧 | Keycloak 26.4(OIDC)+ Spring Security 7.1.1(由 Spring Boot 4.1.1 BOM 管理,實測) |
| 關聯文件 |
SAD-005 §3(身分驗證)、SAD-005 §4(授權)、CSP-003 §3(錯誤封套)、CR-004(請求層錯誤碼)、SDD-007 §5(頁面層級權限) |
| 適用範圍 |
17 個領域服務 + api-gateway/bff-web(跨域規格,非單一服務)。後二者位於 backend/platform/(非 services/),自 2026-09-21 起組態已對齊(CR-010 v1.2/v1.3);惟其 oauth2-resource-server 依賴尚未引入,故入站驗證責任仍在領域服務 |
為何需要本文件:
SCS-001~SCS-021無一涵蓋認證/SSO/閘道,
僅SAD-005 §3有原則性記載。依GOV-003條件 9「無 SCS 不寫程式」,
W-3 SSO 接線前必須先立本規格。
1. 對應需求
| 來源 | 項目數 | 驗證方式 |
|---|---|---|
BRD-003 Authentication |
21 | 登入、MFA、鎖定策略測試 |
BRD-003 SSO |
1 | SSO 登入流程端到端測試 |
⚠️ 本規格僅能驗證其中一部分。MFA、密碼政策、帳號鎖定屬
**身分提供者(Keycloak)**之組態,非本專案程式碼;
可驗證者為「服務端是否正確拒絕未授權請求」。
2. 服務職責與邊界
| 元件 | 職責 | 本期是否實作 |
|---|---|---|
| Keycloak(IdP) | 使用者認證、MFA、密碼政策、帳號鎖定、核發 JWT | ✅ 容器已啟用(本期強化組態) |
| API 閘道 | 驗證簽章與有效期、轉發角色(SAD-005 §3.3 之 GW) |
❌ 尚未建立(見 §3) |
| 領域服務 | 驗證 JWT、抽取角色、端點層授權、資料層級權限 | ✅ 本期 |
| 前端(React 18 + Vite) | 導向 IdP、攜帶 Bearer Token | ✅ 本期(見 §7.8,PKCE) |
3. ⚠️ 與 SAD-005 §3.3 之偏差(誠實揭露)
SAD-005 §3.3 之時序圖假設存在 API 閘道(GW),由其驗證簽章與有效期後轉發角色。
⚠️ 本處原記載有誤,2026-09-21 更正
原寫「
backend/services/下僅有 17 個領域服務,並無 gateway/bff 模組」——此敘述錯誤。
實測:二模組確實存在,位置為backend/platform/api-gateway與backend/platform/bff-web
(平台層,與common-spring等並列,不在services/下),且各有application.yml/
application-local.yml與LayeringTest。連鎖影響(為何要鄭重更正):本句連同 §2「適用範圍:全部 17 個領域服務」
使 gateway/bff 被排除在認證規格之外 →CR-010v1.1 執行時僅套用 17 個領域服務,
二模組之組態未對齊(已於CR-010v1.2/v1.3 補正)。
錯誤陳述會讓後續工作重複漏掉同一批模組,故一併更正,並於下方明列二者之目前狀態。
| 模組 | 位置 | 目前狀態(2026-09-21 實測) |
|---|---|---|
api-gateway(8100) |
backend/platform/api-gateway |
骨架(僅 ApiGatewayApplication + LayeringTest);為 WebFlux(spring-cloud-starter-gateway-server-webflux);無 oauth2-resource-server 依賴 |
bff-web(8101) |
backend/platform/bff-web |
骨架(僅 BffWebApplication + LayeringTest);為 Servlet(spring-boot-starter-web + OpenFeign);無 oauth2-resource-server 依賴 |
本期之替代設計:
SAD-005 §3.3 |
本期實作 | 偏差性質 |
|---|---|---|
| GW 驗證簽章與有效期 | 各領域服務自建 OAuth2 Resource Server 驗證 | 架構偏差 |
| GW 轉發角色給 SVC | 服務自行由 JWT 之 realm_access.roles 抽取 |
同上 |
| SVC 資料層級權限檢查 | 不變 | — |
處置:本期以「各服務自建 resource server」先行;
gateway/bff 模組已存在(見上方更正),但其路由/聚合邏輯尚未實作,
且尚未引入oauth2-resource-server依賴,故驗證責任仍留在各領域服務。
此偏差不影響安全性——JWT 驗證屬冪等純函式,
在閘道或服務端執行皆同等有效;差別僅在責任集中與否。⚠️ 另一項已揭露之落差(非本節範圍,記於
CR-010 §9.5):前端(5173)目前
直接呼叫領域服務(如statement-service8092),未經 gateway(8100)——
即「藍圖裡有 gateway」≠「流量實際跑在 gateway 上」。
4. 身分提供者(Keycloak)組態
4.1 既有基礎設施(已實測 2026-09-19 22:50)
| 項目 | 實測結果 |
|---|---|
| 容器 |
cc-keycloak(quay.io/keycloak/keycloak:26.4,profile iam) |
| realm |
cardcore 匯入成功(Realm 'cardcore' imported) |
| OIDC discovery |
GET /realms/cardcore/.well-known/openid-configuration → HTTP 200
|
| 客戶端 |
cardcore-web(public,PKCE S256)、cardcore-services(M2M,service account) |
| 受眾(audience) |
cardcore-api(由 protocol mapper 注入) |
4.2 本期須對齊 SAD-005 §3.1 之組態修正
SAD-005 §3.1 要求 |
原 realm 組態 | 本期修正 |
|---|---|---|
| JWT 有效期 15 分鐘 | accessTokenLifespan: 900 |
✅ 已符合 |
| 閒置逾時 15 分鐘 |
ssoSessionIdleTimeout: 1800(30 分) |
⚠️ 改為 900 |
| 密碼長度 ≥ 12 | 無密碼政策 | ⚠️ 新增 passwordPolicy
|
| 登入失敗 5 次鎖定 15 分鐘 | 無暴力破解防護 | ⚠️ 新增 bruteForceProtected
|
| MFA | 無 | ⬜ 未實作(見 §10) |
4.3 🔴 角色定義不一致(待 BEAM PMO 裁定,不擅自變更)
| 來源 | 角色 |
|---|---|
SAD-005 §4.2 角色權限矩陣(已核定文件) |
CSR、CSR_SUP、CREDIT、CREDIT_MGR、OPS、RISK、FIN、AUDIT、ADMIN(9 個) |
ops/local/config/keycloak-realm.json |
cardcore-customer、cardcore-csr、cardcore-ops、cardcore-batch(4 個) |
兩者對不上,且非單純命名差異(數量 9 vs 4、語意亦不同)。
- 本期不擅自修改
SAD-005 §4.2(已核定文件),也不擅自擴充 realm 角色
(角色屬業務授權模型,須由業務與資安共同定義)。 - 本期之技術接線以 realm 既有 4 角色為準,僅驗證「服務端是否正確拒絕未授權請求」。
-
待裁定事項(已列
HANDOVER §3.C):- 以
SAD-005 §4.2之 9 角色為準 → 須據此重建 realm 角色與群組;或 - 以 realm 之 4 角色為準 → 須回頭修訂
SAD-005 §4.2。
- 以
5. 程式結構
| 套件/類別 | 位置 | 職責 |
|---|---|---|
SecurityConfig |
platform/common-spring |
SecurityFilterChain:JWT 驗證、端點授權、錯誤回應 |
JwtRoleConverter |
platform/common-spring |
realm_access.roles → GrantedAuthority
|
SecurityProperties |
platform/common-spring |
cardcore.security.* 組態(啟用開關、公開路徑) |
各服務 application*.yml
|
各服務 | spring.security.oauth2.resourceserver.jwt.issuer-uri |
為何放
common-spring:17 個服務皆需相同設定,比照GlobalExceptionHandler
之作法(集中於平台模組,避免逐服務重工——同 W-1KeyProvider之教訓)。
6. 授權規則
6.1 公開路徑(無須認證)
| 路徑 | 理由 |
|---|---|
/actuator/health、/actuator/health/**
|
健康檢查(K8s probe) |
/actuator/info |
版本資訊,無敏感資料 |
/actuator/prometheus |
⚠️ 僅本地/內網——見 §10 |
6.2 需認證路徑
其餘一律須攜帶有效 Bearer Token。
6.3 角色與端點 —— ⚠️ 本期僅實作「認證」,不實作「角色授權」
原設計(本節初稿)擬依下表限制端點:
| 角色 | 可存取 | 備註 |
|---|---|---|
cardcore-csr |
查詢類端點(GET) |
對應 SAD-005 §4.2 之 CSR |
cardcore-ops |
全部端點 | 對應 OPS |
cardcore-batch |
批次相關寫入 | 服務帳號(M2M) |
cardcore-customer |
不開放 | 持卡人應經 BFF,不直連領域服務(見 §10) |
實際未實作,理由(並經實作時再確認):
- 角色定義本身尚未定案(見 §4.3:realm 4 角色 vs
SAD-005 §4.29 角色)。
在角色尚未定案前寫死授權規則,等於把待裁定的業務決策偷偷寫進程式碼。 -
SAD-005 §4.2明載 CSR 可執行掛失/鎖卡等寫入操作
(如「掛失/鎖卡:CSR ✅」),與上表「CSR 僅GET」直接矛盾——
若照上表實作,將造成與核定文件不一致。
故本期之授權規則僅有一條:
anyRequest().authenticated()
(任何有效權杖皆可存取,角色僅抽出备用)。
CC-PLT-4030與AccessDeniedHandler已就位,
待 §4.3 角色模型裁定後填入hasRole(...)規則即可生效。
此為刻意之保守設計——寧可暫時不限制,也不要限制錯。
7. 關鍵邏輯
7.1 JWT 驗證
- 簽章:由
issuer-uri自動取得 JWKS(spring.security.oauth2.resourceserver.jwt.issuer-uri)。 - 驗證項目:簽章、
exp/nbf有效期、aud須含cardcore-api。 - 演算法:Keycloak 預設 RS256。
7.2 角色抽取
- Keycloak 之 realm 角色位於
realm_access.roles(非頂層roles)。 - 轉換為
GrantedAuthority時加上ROLE_前綴(Spring Security 慣例)。 - client 角色(
resource_access)本期不使用。
7.3 錯誤回應(對齊 CR-004)
| 情境 | HTTP | 錯誤碼 |
|---|---|---|
無 Authorization 標頭/格式非 Bearer/權杖過期/簽章不符 |
401 |
CC-PLT-4010(未認證) |
| 已認證但權限不足 | 403 |
CC-PLT-4030(無權限) |
CC-PLT-4010/CC-PLT-4030為本期新增:CR-004之CC-PLT-4xxx
五碼僅涵蓋 400/404/405,無 401/403;且認證攔截發生在進入控制器之前,
不在GlobalExceptionHandler之@ControllerAdvice範圍內。
依CSP-003 §2「不得自創錯誤碼,新增碼須先擴充SDD-006 §3.4」程序辦理:
已補登SDD-006 §3.4並重跑tools/gen_error_codes.py(112 → 114 碼)。⚠️ 實測修正(重要):
oauth2ResourceServer自帶
BearerTokenAuthenticationEntryPoint,會蓋掉
exceptionHandling()設定之 entry point,
且其預設回應主體為空(實測Content-Length: 0)——
違反CSP-003 §3「所有回應一律套用ApiResponse封套」。
故必須在oauth2ResourceServer(...)內再設一次相同 handler(見實作註解)。
WWW-Authenticate回應Bearer;失敗時另附 RFC 6750 錯誤碼
(如Bearer error="invalid_token"),
不含error_description——避免外洩函式庫內部訊息(CSP-004 §6)。
7.4 組態開關
cardcore:
security:
enabled: true # **預設 true(fail-closed)**——見下方「v1.7 改為 fail-closed」
permit-all: # 公開路徑
- /actuator/health/**
- /actuator/info
⚠️ v1.7(2026-09-20,BEAM PMO 核定):改為 fail-closed(預設
true)原設計刻意預設
false,理由為「避免服務引入common-spring後意外被要求認證」。
該理由本末倒置:其失敗模式是靜默放行——
與 Flyway 未顯式引入 starter、loki exporter 不存在、OTel 手動組合不匯出
同屬「靜默不做」型缺陷(見SCS-020 §9.2),是最危險的一類。改為 fail-closed 後之三種情境(皆已實測,見 §9.2)
情境 行為 實測 未設定(預設) 要求認證 → 無憑證回 401 CC-PLT-4010✅ 啟用但無 issuer-uri啟動即失敗: required a bean of type 'JwtDecoder'(大聲失敗,勝過靜默放行)✅ 本地開發 由 application-local.yml顯式停用(enabled: ${CARDCORE_SECURITY_ENABLED:false}),並記 WARN✅ 17/17 停用責任歸屬:停用只能顯式宣告,且僅限本地檔
(該檔「僅供本地,禁止流入 SIT/UAT/PROD」)。
前端VITE_AUTH_ENABLED亦同步改為預設true(見 §7.8 組態表)——
兩端預設值必須同調,否則「前端關、後端開」會全部 401。
7.9 出站(服務間呼叫)憑證——client_credentials(CR-010,2026-09-20 BEAM PMO 核定採 A)
入站與出站是兩條腿。 §7.4 處理的是「別人打進來要不要認證」;
本節處理「我打給別人要不要帶憑證」。缺了後者,一啟用認證就全掛。
問題(實測,非推測):CARDCORE_SECURITY_ENABLED=true 下執行帳單週期 →
statement→posting、posting→pricing 全數 401;
批次因而回 GENERATED 但 statementCount=0(正常完成卻什麼都沒做)。
根因:ServiceClientHttpClient 之建構子僅收逾時組態,無任何憑證來源,
呼叫端亦從未帶 Authorization 標頭——端到端都沒有這個能力。
IdP 端其實早已備妥(實測):Keycloak realm cardcore 之
cardcore-services 為 confidential client、serviceAccountsEnabled=true、
具 audience-cardcore-api mapper。實測 client_credentials 取 token:
HTTP 200、expires_in=900、aud=["cardcore-api","account"]。
組態(cardcore.security.outbound.*)
| 鍵 | 預設 | 說明 |
|---|---|---|
token-uri |
無(必填) |
${KEYCLOAK_TOKEN_URI},本地預設 Keycloak token 端點 |
client-id |
cardcore-services |
M2M client |
client-secret |
無(必填) |
${CARDCORE_M2M_CLIENT_SECRET},不給預設值、不得進 repo
|
cache-skew |
30s |
提前多久視為過期(吸收時鐘誤差) |
connect-timeout/request-timeout
|
2s/5s
|
取 token 之逾時,與業務呼叫分開(見下) |
⚠️ 逾時不得沿用業務呼叫之預設值:取 token 是基礎設施呼叫,
不應重試(4xx 表憑證有誤),且 IdP 可能冷啟動,故請求逾時較寬(5s vs 3s)。
呼應既有教訓——每接一個外部系統都要重新檢視預設值。
類別(platform/common-spring)
| 類別 | 職責 |
|---|---|
security.outbound.OutboundTokenProvider |
介面:authorizationHeaderValue()
|
security.outbound.ClientCredentialsTokenProvider |
取 token、快取、到期前續取;失敗拋錯不降級 |
security.outbound.NoOpOutboundTokenProvider |
本地顯式停用時使用,記 WARN |
security.outbound.OutboundTokenAutoConfiguration |
組裝 bean 與 fail-closed 守門 |
client.ServiceClientHttpClient |
送出前帶 Authorization;憑證來源 requireNonNull
|
fail-closed 三情境(皆已實測)
| 情境 | 行為 |
|---|---|
| 入站啟用 + 出站憑證齊備 | 取 token 並帶出 |
| 入站啟用 + 出站憑證缺漏 |
啟動期失敗(IllegalStateException,訊息直接列出缺什麼) |
| 入站顯式停用(本地) | 不帶憑證,記 WARN(可與「忘了設定」區分) |
為何不是「取不到 token 就照發」:那正是失敗即放行(fail-open),
與 §7.6 已處置之「宣告啟用卻沒依賴 → 靜默放行」同型。為何與入站共用
cardcore.security.enabled而不另立開關:
兩邊必須同調——入站要認證、出站卻不帶憑證 → 全部 401;
反之「以為有鎖其實沒鎖」更危險。理由與前後端VITE_AUTH_ENABLED同調一致(§7.4)。
實測(2026-09-20,真實三服務)
以 CARDCORE_SECURITY_ENABLED=true 啟動 statement(8092)/posting(8087)/pricing(8086):
| 步驟 | 結果 |
|---|---|
| 三服務啟動日誌 | 三個皆見「服務間呼叫憑證已啟用:client_credentials」+「認證機制已啟用:JWT」 |
| 無 token 呼叫批次端點 |
401 CC-PLT-4010(入站確有鎖) |
以 client_credentials 取 token |
200,長 1,307 |
| 帶 token 執行 2026-12 週期 | 201 GENERATED、processed=1、statementCount=1 |
| 三個服務日誌之 401 次數 | 0/0/0 |
| DB | 新增 STMT2026120010(prev=71,309 正確銜接 11 月、new=72,341) |
憑證管理
-
client-secret一律由環境變數CARDCORE_M2M_CLIENT_SECRET提供。 - 本地置於
~/.config/cardcore/m2m.env(權限 600、在 repo 外),
由ops/local/env.sh「若存在才載入」。 - SIT/UAT/PROD 須由金鑰管理系統(Vault/KMS)注入。
- ⚠️ 既有狀況:
ops/local/config/keycloak-realm.json內含一組開發用
cardcore-services-dev-secret(本機 Keycloak 匯入用,非本次引入),
真實環境不得比照。
7.5 ⚠️ Spring Boot 4 自動配置模組化陷阱(實測,非推測)
SecurityAutoConfiguration 需以 ObjectMapper 寫出錯誤封套。
Spring Boot 4 已將自動配置與測試自動配置拆分為獨立模組,
套件路徑與 3.x 不同——逐項皆為解開 jar 實測所得:
| 類別 | Spring Boot 3.x | Spring Boot 4.1.1(實測) | 所需依賴 |
|---|---|---|---|
JacksonAutoConfiguration |
org.springframework.boot.autoconfigure.jackson |
org.springframework.boot.jackson.autoconfigure |
spring-boot-jackson |
@AutoConfigureMockMvc |
org.springframework.boot.test.autoconfigure.web.servlet |
org.springframework.boot.webmvc.test.autoconfigure |
spring-boot-webmvc-test |
第二個陷阱(更危險):即使套件正確,以
@AutoConfiguration(after = JacksonAutoConfiguration.class)
排序仍無法保證ObjectMapperbean 可取——實測啟動失敗
(ObjectMapper尚未註冊)。
故改為ObjectProvider<ObjectMapper>惰性取得:
有應用程式之ObjectMapper就用它(序列化格式一致);沒有則退回內建實例。
徹底去除自動配置順序依賴——此類「靜默型」缺陷不會在編譯期報錯,
只會在執行期以奇怪的方式失敗(同Spring Boot 4 + Flyway之教訓)。
7.6 ⚠️ 依賴守門:宣告啟用卻沒依賴時,啟動期失敗(fail-closed)
問題(實測發現,非推測):SecurityAutoConfiguration 以
@ConditionalOnClass({SecurityFilterChain, HttpSecurity, Jwt}) 宣告。
服務若未引入 spring-boot-starter-oauth2-resource-server,該自動配置會被
靜默跳過——此時即使設定 cardcore.security.enabled=true,
所有請求仍全部放行,且啟動日誌不會有任何錯誤。
實測紀錄(2026-09-19):
card-service設CARDCORE_SECURITY_ENABLED=true
但未引入依賴 → 無 token 亦回 HTTP 200,且日誌無任何警示。
此為失敗即放行(fail-open):認證宣告已啟用卻未生效,
等同系統在管理者不知情的情況下裸奔——信用卡核心不可接受。
對策:新增 SecurityDependencyGuardAutoConfiguration——
當「cardcore.security.enabled=true」且「classpath 無 Jwt」時,
於啟動期拋 IllegalStateException,並於訊息中直接給出處理方式。
依賴存在時本類不生效(由 SecurityAutoConfiguration 接手);
未宣告啟用時亦不生效(維持「預設關閉」之設計)。
⚠️ 本類不得 import 任何 Spring Security 類別——
其存在目的即在「依賴缺席」時仍能載入,故僅以類別名字串
(@ConditionalOnMissingClass)判定。
實測驗證:card-service 於缺依賴時重啟 → 啟動失敗並顯示上述訊息;
補上依賴後重啟 → 認證機制已啟用:JWT(OAuth2 Resource Server),
隨後 401/200/偽造 token/公開路徑四情境皆如預期。
7.7 單一服務之導入步驟(onboarding)
| 步驟 | 動作 | 本期狀態 |
|---|---|---|
| ① |
build.gradle.kts 加入 implementation("spring-boot-starter-oauth2-resource-server")
|
✅ 17/17 領域服務已完成(2026-09-20,本次補齊其餘 15 個)。⚠️ api-gateway/bff-web 仍未引入(見下表) |
| ② |
application-local.yml 設定 spring.security.oauth2.resourceserver.jwt.issuer-uri
|
✅ 19/19 已完成(2026-09-21:api-gateway/bff-web 依下方「預先就位」慣例補齊) |
| ③ | 設 cardcore.security.enabled
|
✅ 預設即為 true(fail-closed,v1.7 起);本地由 application-local.yml 顯式停用(19/19);CARDCORE_SECURITY_ENABLED=true 可於本地開啟驗證 |
② 與 ① 分離之理由:組態先行就位,使「啟用」退化為單一開關;
未導入依賴者若誤設開關,由 §7.6 之守門於啟動期擋下,不會靜默放行。
api-gateway/bff-web 之導入狀態(2026-09-21 實測):
| 步驟 | 狀態 | 說明 |
|---|---|---|
② issuer-uri
|
✅ 已就位(19/19) | 依「含尚未導入依賴者,預先就位」之既有慣例補上,使未來補 ① 時不遺漏 |
③ cardcore.security.enabled + outbound
|
✅ 已就位(19/19,CR-010 v1.2) |
本地顯式停用;一旦啟用,由 SecurityDependencyGuardAutoConfiguration 啟動期失敗並明指缺 ① |
① oauth2-resource-server 依賴 |
⚪ 未導入 | 二模組仍為骨架;承擔入站驗證職責前須補。此為已知落差,非缺陷——守門使其「大聲失敗」而非靜默放行 |
⚠️ 非本地環境(SIT/UAT/PROD)刻意不在
application.yml設cardcore.security:
依 §7.4「停用只能顯式宣告,且僅限本地檔」,非本地即以預設 true(fail-closed) 運作。
實測 19 份application.yml皆無該區段——此為設計如此,不是缺漏;
憑證一律由環境變數(KMS)注入,不進 repo。
③ 之單一開關:CARDCORE_SECURITY_ENABLED 經 Spring Boot 寬鬆綁定對應
cardcore.security.enabled,故無須逐服務改 yml——一次環境變數即全域生效:
export CARDCORE_SECURITY_ENABLED=true # 後端 17 服務
export VITE_AUTH_ENABLED=true # 前端(必須同調,見 §7.8)
7.7.1 逐服務實測結果(2026-09-20,17/17)
以 CARDCORE_SECURITY_ENABLED=true 逐一起 jar 實測(探測路徑 /api/v1/sso-probe,
該路徑不存在,故帶 token 之 404 即代表通過認證、未達授權階段):
| 服務 | 埠 | 無 token | 帶 token | /actuator/health |
|---|---|---|---|---|
| authorization-service | 8081 | 401 | 404 | 200 |
| card-service | 8082 | 401 | 404 | 200 |
| limit-service | 8083 | 401 | 404 | 200 |
| party-service | 8084 | 401 | 200(真實資料) | 200 |
| product-service | 8085 | 401 | 404 | 200 |
| pricing-service | 8086 | 401 | 404 | 200 |
| posting-service | 8087 | 401 | 404 | 200 |
| payment-service | 8088 | 401 | 404 | 200 |
| installment-service | 8089 | 401 | 404 | 200 |
| rewards-service | 8091 | 401 | 404 | 200 |
| statement-service | 8092 | 401 | 404 | 200 |
| dispute-service | 8093 | 401 | 404 | 200 |
| clearing-service | 8094 | 401 | 404 | 200 |
| merchant-service | 8095 | 401 | 404 | 200 |
| migration-service | 8096 | 401 | 404 | 200 |
| notification-service | 8097 | 401 | 404 | 200 |
| report-service | 8098 | 401 | 404 | 200 |
party-service係以真實存在的持卡人驗證(P001),故為 200 而非 404。
7.8 前端(WebUI)取得 Token——PKCE(HANDOVER #C-37,BEAM PMO 2026-09-20 核定採 A)
為何需要:後端 17 服務之驗證機制已於 §7.1~§7.7 完成並實測,
但frontend/未帶任何Authorization標頭,
致cardcore.security.enabled常態必須維持 false——
否則 WebUI 全數 401。⚠️ v1.7 起此限制已解除:本節之 PKCE 已完成,前端已可帶 token,
故上述「常態必須維持 false」之理由不再成立——
已於 v1.7 改為 fail-closed(預設 true),詳 §7.4。
本節為「前端如何取得 token」之規格。
已備妥之基礎設施(實測 2026-09-20 07:50,非推測)
| 項目 | 實測結果 |
|---|---|
Keycloak client cardcore-web
|
已存在:publicClient=true、standardFlowEnabled=true、directAccessGrantsEnabled=**false**(不開 password grant)、webOrigins=http://localhost:5173
|
redirectUris |
http://localhost:5173/*——與 Vite dev server 埠號一致
|
| 發現端點 |
grant_types_supported 含 authorization_code;code_challenge_methods_supported 含 S256
|
→ IdP 端已就位,本期僅須於前端實作授權碼流程。
流程(Authorization Code + PKCE,RFC 7636)
| 步驟 | 內容 |
|---|---|
| ① | 產生 code_verifier(43~128 字元之高熵亂數)與 state;code_challenge = BASE64URL(SHA256(code_verifier)),方法固定 S256(不用 plain) |
| ② | 導向 authorization_endpoint,帶 response_type=code、code_challenge、code_challenge_method=S256、state
|
| ③ | IdP 回導 redirect_uri?code=…&state=…;先比對 state(防 CSRF),不符即中止 |
| ④ | 以 code + code_verifier 向 token_endpoint 交換 token(不帶 client_secret——public client 沒有) |
| ⑤ | 取得後立即移除 code_verifier(一次性,不得留存) |
Token 存放(安全取捨)
| 項目 | 存放位置 | 理由 |
|---|---|---|
| access token | 記憶體(模組層變數) | 不落地即不受 XSS 竊取持久憑證;重整頁面即失效 |
| refresh token | sessionStorage |
關掉分頁即清除;若存記憶體則重整就必須重新登入,demo 體驗過差 |
code_verifier/state
|
sessionStorage(用後即刪) |
流程跨導頁,必須暫存;一次性 |
⚠️ 不得使用
localStorage——它跨分頁持久且同源任何腳本皆可讀,
一旦發生 XSS 即為長期憑證外洩。此為 SPA 之最低安全底線。
換發與失效
| 情境 | 處理 |
|---|---|
| access token 將屆期(提前 60 秒) | 自動以 refresh token 換發 |
收到後端 401 CC-PLT-4010
|
僅重試一次:先換發再重送原請求;仍 401 才導向登入 |
| refresh 失敗 | 清除 token,導向 IdP 重新認證 |
| 登出 | 呼叫 end_session_endpoint 並清除本端 token |
401 之所以「只重試一次」:若無條件重試,token 真的失效時會形成請求風暴。
不引第三方 OIDC 套件(刻意決定)
本期手寫精簡 PKCE 流程,不導入 keycloak-js/oidc-client-ts。理由:
① 銀行案資安審查,每多一個第三方套件就多一份待審相依;
② 所需僅「授權碼+PKCE+refresh+登出」,手寫約百餘行即可,可控性高於引入框架;
③ 前端既有風格為裸 fetch,引入套件會造成兩種呼叫風格並存。
組態開關
| 變數 | 預設 | 說明 |
|---|---|---|
VITE_AUTH_ENABLED |
true(v1.7 起,與後端同調) |
關閉時不導向 IdP、不帶標頭——與後端 cardcore.security.enabled 必須同調;兩端預設值皆為 fail-closed,本地由 .env.development 顯式停用 |
VITE_KEYCLOAK_URL |
http://localhost:8180 |
IdP 根位址 |
VITE_KEYCLOAK_REALM |
cardcore |
realm |
VITE_KEYCLOAK_CLIENT_ID |
cardcore-web |
public client |
組態一律經
import.meta.env注入,不得硬編於程式碼(CSP-001 §8 B6)。
實測證據(2026-09-20,真實 Keycloak + 真實 Oracle + 真實 Chrome)
① 協定層(tools/verify_sso_pkce.py,僅用標準函式庫):
| 情境 | 結果 |
|---|---|
| 無 token |
401 CC-PLT-4010
|
| 帶 PKCE 取得之 token |
200,回傳真實持卡人 P001(個資欄位皆為遮蔽值) |
| 偽造 token(末 6 碼改寫) |
401 CC-PLT-4010
|
| refresh_token 換發 | 200,取得新 access token |
該腳本並含反向驗證:對未啟用安全性之服務執行時會判定 ❌
(實測posting-service即為此情形——故其同時為HANDOVER #C-34
「SSO 推廣至其餘 15 服務」之實證)。
② 瀏覽器層(真實 Chrome headless,Vite dev server 5173):
| 步驟 | 結果 |
|---|---|
| 未登入訪問前端 | 導向 Keycloak 授權端點 |
輸入 demo.holder
|
回導 5173,前端完成交換後清除網址上的 code
|
| token 存放 | refresh 於 sessionStorage ✅;localStorage 為空 ✅ |
執行前端自己的 fetchParty()
|
200,取回 P001/王小明;送出的 /party-api 請求帶 Bearer
|
| 重整(access token 存於記憶體已失) | 以 refresh token 重建 session 成功,不必重打帳密 |
瀏覽器層之驗證係直接
import('/src/api/parties.ts')執行前端程式碼,
非另寫一份等效邏輯——故驗證到的確實是交付出去的那份程式。
7.10 服務間呼叫 ACL(白名單)——CR-011 階段二(2026-09-21)
本節為「授權」之第二層:§7.1–7.4 解決「是不是有效權杖」(認證),
§7.9 解決「呼叫時有沒有帶權杖」(出站憑證),本節解決「這個權杖的持有者能不能打這個服務」。
7.10.1 為何需要
CR-010 之後,任何持有有效 M2M 權杖者可呼叫任何服務的任何端點——
等於「有鑰匙就能開所有的門」。SAD-002 §5 已定義服務互動矩陣(誰可以同步呼叫誰),
但該矩陣從未被程式讀取,形同具文。本節將其落實為可執行之白名單。
7.10.2 單一真實來源
SAD-002 §5 互動矩陣 ──(tools/gen_service_acl.py --write)──▶ cardcore-service-acl.yml
- 輸出:
backend/platform/common-spring/src/main/resources/cardcore-service-acl.yml(19 條目) -
⚠️ 本檔不得手改。改動流程:修
SAD-002 §5→ 重跑產生器 → 重跑測試。
直接編輯 yml 會造成「規格與實作兩份真相」。 - 轉置規則:矩陣為「呼叫者 \ 被呼叫者」,本檔為「被呼叫端 → 允許之
azp」;只取同步(S),
事件(A)走 Kafka、不受本機制管制。
7.10.3 判斷規則(ServiceCallerAclManager)
| 順序 | 情境 | 結果 |
|---|---|---|
| 1 | 未認證/匿名 | 拒絕(401) |
| 2 | 使用者 token(azp 非 cc- 開頭) |
放行——交由角色授權,本 ACL 不管制 |
| 3 | 服務帳號(azp = cc-<模組名>)且列於本服務之允許清單 |
放行 |
| 4 | 服務帳號但不在允許清單 |
拒絕(403 CC-PLT-4030) |
⚠️ 規則 2 之理由:若一併管制使用者 token,將擋掉所有使用者請求
(azp 為前端 client,不在互動矩陣內)。使用者能做什麼屬「角色授權」,與「誰打來」正交,須分開。
7.10.4 fail-closed
| 情境 | 行為 |
|---|---|
| ACL 資源不存在 | 啟動期拋錯 |
| ACL 解析失敗 | 啟動期拋錯(不靜默回傳空 ACL) |
spring.application.name 未設定 |
啟動期拋錯 |
本服務不在 ACL 中(=SAD-002 §5 未涵蓋) |
啟動期拋錯 |
「沒有設定」與「設定為空」必須可區分:後者代表「本服務不接受任何同步呼叫」(如
statement-service),
前者代表「規格有缺漏」——都當成空集合會讓缺漏靜默通過。
7.10.5 🔴 已知陷阱:AnonymousAuthenticationToken.isAuthenticated() 為 true
Spring Security 於無憑證請求時放入 AnonymousAuthenticationToken,且其 isAuthenticated() 回傳 true
(刻意設計,使其能參與授權判斷)。因此:
// ❌ 錯誤:匿名請求之 auth 非空且 isAuthenticated()==true,
// 會落入「非服務帳號一律放行」而被放行(實測回 200)
if (auth == null || !auth.isAuthenticated()) { return deny; }
// ✅ 正確:顯式排除匿名
if (auth == null || !auth.isAuthenticated()
|| auth instanceof AnonymousAuthenticationToken) { return deny; }
此缺陷係由過濾器層級測試(
ServiceAclFilterTest ④)抓出——
單元測試之負樣本誤用TestingAuthenticationToken(false),非真實型別,故未發現。
教訓:負樣本必須覆蓋所有判斷點,且必須用真實型別。
7.10.6 實測(2026-09-21,真實 Keycloak token → 真實 posting-service)
| # | 呼叫者 | 結果 |
|---|---|---|
| ① | 無憑證 |
401 CC-PLT-4010
|
| ② |
cc-statement-service(矩陣 S) |
404 CC-POST-4040 —— ACL 通過(查無分戶,業務錯誤) |
| ③ |
cc-clearing-service(矩陣非 S) |
403 CC-PLT-4030 ✅ |
| ④ | cc-bff-web |
403 CC-PLT-4030 ⚠️ —— 見 §10 |
啟動日誌:服務間呼叫 ACL 已載入(19 個條目,本服務允許 4 個呼叫者);
拒絕時記 ⛔ 服務間呼叫遭拒:本服務=posting-service 呼叫者=cc-clearing-service,traceId 與 HTTP 回應一致。
7.10.7 測試(判定力已驗證)
| 測試 | 例數 | 驗證層級 |
|---|---|---|
ServiceAclTest |
10 | 單元(解析/fail-closed/AuthorizationManager 決策) |
ServiceAclFilterTest |
4 |
過濾器層級(走完整 filter chain,防「沒掛上 HttpSecurity」之假綠) |
負樣本驗證:決策一律放行 → 5 例轉紅;一律拒絕 → 3 例轉紅。
common-spring 測試總數 71 → 85,全綠。
7.11 使用者權杖轉傳(Token Relay)——gateway/bff 採 B(BEAM PMO 2026-09-21 17:10 核定)
設計依據:
SAD-005 §3.4(身分轉傳原則)/CR-011 §11。
本節為其程式設計規格;裁定內容見CR-011 §11。
7.11.1 為何需要(問題陳述)
CR-011 階段二上線後,實測 cc-bff-web/cc-api-gateway 呼叫領域服務 → 403 CC-PLT-4030。
根因:SAD-002 §5 互動矩陣不含 gateway/bff,故二者之允許清單皆為 [](產生器轉置之正確結果)。
兩個選項中,BEAM PMO 核定採 B——不為其開 M2M 白名單,而令其轉傳使用者權杖:
| 做法 | 後果 | |
|---|---|---|
| A(不採) | 於矩陣新增 gateway/bff 兩列 | 取得「可呼叫所有服務」之超級身分;服務端失去使用者身分 |
| B(採) | 轉傳使用者權杖,不以 M2M 呼叫領域服務 | 現行 ACL 無須修改;azp 非 cc- 本就不受管制(§7.10 規則 3) |
7.11.2 組態模式(cardcore.security.outbound.mode)
| 模式 | 適用 | 憑證需求 | 行為 |
|---|---|---|---|
client_credentials(預設) |
17 個領域服務;非使用者情境(批次/排程/事件消費) |
token-uri/client-id/client-secret 三項必填
|
由 ClientCredentialsTokenProvider 取 M2M 權杖 |
token-relay |
僅 api-gateway/bff-web |
無(不得也不需要設定 client 憑證) | 由 RelayingOutboundTokenProvider 轉傳當前使用者權杖 |
⚠️ 為何以「模式切換」而非「刪除 gateway/bff 之出站組態」:
OutboundTokenAutoConfiguration之 fail-closed 為「入站啟用 + 出站憑證未設定 → 啟動期失敗」。
直接刪除組態會使二者在enabled=true時起不來。故必須新增模式,讓「不需要 M2M 憑證」成為可被表達的合法狀態。
7.11.3 RelayingOutboundTokenProvider 行為規格
| # | 情境 | 行為 |
|---|---|---|
| 1 | 當前身分為 JwtAuthenticationToken
|
回 Bearer <原權杖字串>(原樣轉傳,不重新簽發、不改內容) |
| 2 | 無身分/匿名(AnonymousAuthenticationToken) |
拋 IllegalStateException(fail-closed) |
| 3 | 身分型別非 JWT |
拋 IllegalStateException——不靜默降級為不帶標頭 |
⚠️ 情境 2/3 之所以拋錯而非回
null:回null會讓呼叫「不帶Authorization照發」,
屬失敗即放行(fail-open),與§7.6已處置之「宣告啟用卻沒依賴 → 靜默放行」同型,不可接受。
批次情境下該錯誤還會被「跳過該帳戶、批次繼續」吸收成靜默零產出(CR-010之 P0 實例)。
7.11.4 與服務間 ACL(§7.10)之關係
轉傳後領域服務見到之 azp 為 cardcore-web(非 cc-)→ 判定「非服務帳號」→ 放行,
交由角色授權。故白名單無須任何修改——這是 B 方案之關鍵優勢。
7.11.5 已知限制(誠實揭露)
| # | 限制 |
|---|---|
| 1 |
僅適用於同步、同執行緒之呼叫。SecurityContext 預設不跨執行緒傳遞;@Async/反應式(WebFlux)情境須另行處理 |
| 2 | gateway/bff 目前仍為骨架(無路由、無 Feign client),本節係機制先行;實際轉傳行為尚未有端到端實測(無呼叫端可測) |
| 3 | 轉傳之權杖若於飛行途中過期,被呼叫端將回 401;本機制不主動刷新(應由前端負責,見 §7.8) |
| 4 | 領域服務無法區分「直接來自瀏覽器」與「經 bff 轉傳」——二者之 azp 皆為 cardcore-web。若日後需要區分,須另於權杖中加入來源宣告 |
8. 外部依賴與降級
| 依賴 | 失敗處理 |
|---|---|
| Keycloak 不可用(JWKS 抓取失敗) | 啟動期:JWKS 快取未建立 → 服務啟動失敗(fail-fast,不靜默放行) |
| Keycloak 事後不可用 | 運行期:已快取之 JWKS 仍可驗證 → 既有 token 仍可用;換 key 後始失效 |
| 時鐘偏移 | 依賴 NTP;exp 驗證容許預設之時鐘偏移 |
⚠️ 不提供「繞過認證」之降級——認證失敗一律拒絕,不得放行(資安底線)。
9. 測試案例
| 編號 | 情境 | 預期 |
|---|---|---|
UT-SEC-001 |
無 Authorization 標頭 |
401 CC-PLT-4010
|
UT-SEC-002 |
格式錯誤(非 Bearer) |
401 CC-PLT-4010
|
UT-SEC-003 |
有效 token | 200 且可取得資料(角色未納入判定,見 §6.3) |
UT-SEC-004 |
有效 token、角色不符 | ⬜ 未實作——見 §6.3,待角色模型裁定(CC-PLT-4030 目前不會被觸發) |
UT-SEC-005 |
過期 token | 401 |
UT-SEC-006 |
簽章不符(偽造 token) | 401 |
UT-SEC-007 |
公開路徑 /actuator/health
|
200 無須 token |
UT-SEC-008 |
關閉開關(enabled: false) |
全部路徑放行(本地顯式停用用;本地/測試不得仰賴預設值) |
UT-SEC-010 |
不設定開關(驗證 fail-closed) |
401 CC-PLT-4010——證明「忘記設定」時是拒絕、不是放行。見 SecurityFailClosedTest(已含反向驗證:預設改回 false 時該測試必紅) |
IT-SEC-001 |
向 Keycloak 取得真實 token 並呼叫服務 | 200(端到端,證明非自嗨) |
UT-SEC-009 |
角色授權(403) | ⬜ 未實作——見 §6.3,待角色模型裁定 |
9.1 實測證據(2026-09-19,向 Keycloak 取得真實權杖)
| # | 情境 | 實測結果 |
|---|---|---|
| ① | 無 Authorization 標頭 |
401 CC-PLT-4010(含封套) ✅ |
| ② | 有效權杖(Keycloak 核發) |
200,回傳真實資料 P001/0912****78/k***@example.com ✅ |
| ③ | 公開路徑 /actuator/health
|
200(無須權杖) ✅ |
| ④ | 偽造權杖(簽章不符) |
401 CC-PLT-4010;WWW-Authenticate: Bearer error="invalid_token";主體含封套 ✅ |
| ⑤ | 格式錯誤(Basic 而非 Bearer) |
401 CC-PLT-4010 ✅ |
| ⑥ | 過期權杖(逾 15 分鐘) |
401 CC-PLT-4010 ✅ |
權杖宣告實測:
iss=http://localhost:8180/realms/cardcore、
aud=cardcore-api、realm_access.roles=["cardcore-csr"]
——確認角色位於巢狀之realm_access.roles(見 §7.2)。
9.2 fail-closed 實測證據(HANDOVER #C-24,2026-09-20 12:10)
以 card-service(埠 8082)+ 真實 Oracle + 真實 Keycloak 26.4 實測三種情境:
| # | 情境 | 實測結果 |
|---|---|---|
| ① |
本地預設(application-local.yml 顯式 enabled: false) |
啟動日誌 認證機制【已顯式停用】;/actuator/health 200;發卡請求未被認證擋下(400/201,取決於請求內容,非 401) ✅ |
| ② | CARDCORE_SECURITY_ENABLED=true |
啟動日誌 認證機制已啟用:JWT(OAuth2 Resource Server);無 token → 401 且主體 {"error":{"code":"CC-PLT-4010","message":"未提供有效之認證憑證"}};帶真實 Keycloak token → 201(真的放行並建卡); /actuator/health(permitAll)200 ✅ |
| ③ | 啟用但未給 issuer-uri
|
啟動失敗(APPLICATION FAILED TO START):Method cardcoreSecurityFilterChain ... required a bean of type 'org.springframework.security.oauth2.jwt.JwtDecoder';結束碼 1、埠無監聽 ✅ |
③ 之意義:fail-closed 有兩種表現——「拒絕請求(401)」與「啟動失敗」。
後者看似粗暴,但遠勝靜默放行:組態缺失會在部署當下被發現,
而不是等到被稽核或被打穿才發現。
守門測試:platform/common-spring 新增
SecurityFailClosedTest——刻意不設定 cardcore.security.enabled,
斷言無憑證請求為 401。
已做反向驗證:將 SecurityProperties.enabled 暫改回 false 後,
該測試確實轉紅(FAILED),故非假驗證。
10. 已知限制(誠實揭露)
-
無 API 閘道——見 §3,驗證責任分散於各服務。
-
角色定義不一致——見 §4.3,待 BEAM PMO 裁定。
-
MFA 未啟用——
SAD-005 §3.1要求 MFA,Keycloak 需設定 Authentication Flow;
本期未做(屬 IdP 組態,非程式碼)。 -
→ 已解除:本期已補登401無專屬錯誤碼CC-PLT-4010/CC-PLT-4030
(SDD-006 §3.4,錯誤碼 112 → 114)。
惟CC-PLT-4030目前不會被觸發——因角色授權未實作(見 §6.3)。 -
/actuator/prometheus未納入授權——監控抓取通常不帶 token;
本地可接受,正式環境須由網路層(內網/Service Mesh)隔離,不得以授權放行。 -
持卡人(
cardcore-customer)直連領域服務本期不開放——
持卡人應經 BFF(SAD-001 AP-*),直連會繞過資料層級權限。 -
服務間認證之 mTLS 半邊仍未實作——
SAD-005 §3.2要求「mTLS + Service Token」;
2026-09-20 之CR-010已補上 Service Token(client_credentials,見 §7.9),
惟 mTLS 仍待基礎設施(憑證簽發與輪替),未因本次而宣稱完成。 -
敏感操作再驗證(
SAD-005 §3.1)未實作——須搭配SDD-007 §5之頁面流程。 -
實測覆蓋僅 2/17 服務→ ✅ 已解除(17/17):本次補齊其餘 15 個服務之
oauth2-resource-server依賴並逐服務實測,詳 §7.7.1。
其僅完成「認證」之實測——授權(角色)仍未實作(見 §6.3),
故帶 token 之結果為 404(未達授權階段)而非角色判斷之結果。 -
🔴 前端(WebUI)無 token 來源→ ✅ 已解除:本期實作 §7.8 之 Authorization Code + PKCE(手寫、不引第三方套件),access token 存記憶體、refresh token 存sessionStorage、不用localStorage;401 僅重試一次、refresh 失敗才重新導向。
⚠️ 仍存之限制:SPA 於瀏覽器持有 token 非銀行生產之最終型態——最安全者為「token 不進瀏覽器」之 BFF/Gateway 模式(httpOnly cookie),待C-12(api-gateway)/C-13(bff-web)建立後應將此責任上收。本期定位為 POC/展示環境之可用方案,已於此明列以供審查。 -
Keycloak 健康端點 404→ 已於 v1.6 查明並修正(HANDOVER #C-35):
端點本來就正常,問題是它不在主埠 8180,而在管理埠 9000(官方文件:
「exposed on the management port 9000 by default」),而該埠未映射至主機
→ 由主機探測/health*一律 404,易被誤判為「健康端點壞掉」。
實測容器內9000/health/ready→ 200{"status":"UP"}(含資料庫連線檢查)。
修正:①docker-compose.yml映射9000:9000②補KC_METRICS_ENABLED=true
③prometheus.yml新增keycloakjob(原完全沒有指標)。
現況:/health//health/ready//health/live//health/started皆 200;
Prometheusup{job="keycloak"}=1、930 個序列。
回歸驗證:容器重建後權杖簽發仍正常(demo.holder→cardcore-customer)。
⚠️ 資安提醒:管理埠不應對外暴露——本機可接受,SIT 以上應限監控網段,
或改以容器內 HEALTHCHECK 探測(免映射埠)。 -
⚠️ 認證「機制已備」但「常態未啟用」→ 已於 v1.7 解除(BEAM PMO 2026-09-20 核定):
SecurityProperties.enabled預設改為 true(fail-closed)。
原「忘記設環境變數即靜默放行」之風險已消除——
未設定時為要求認證(401),連issuer-uri都沒給則啟動即失敗(§9.2 ③)。
停用改為只能顯式宣告,且僅限application-local.yml(17/17 皆已宣告),
停用時記 WARN 使「刻意停用」與「忘了啟用」在日誌上可區分。
前端VITE_AUTH_ENABLED同步改為預設true(兩端必須同調,見 §7.4)。 -
✅
api-gateway/bff-web之呼叫方式——BEAM PMO 2026-09-21 17:10 核定採 B(轉傳使用者權杖),並已執行完畢(CR-011 §11):
實測以cc-bff-web之權杖呼叫posting-service→ 403CC-PLT-4030。
原因:SAD-002 §5互動矩陣僅涵蓋 17 個領域服務,不含 gateway/bff →
二者既不允許任何服務帳號呼叫,也未被任何服務允許。
現況:兩模組皆為骨架(api-gateway無路由設定、bff-web無 Feign client),
故無立即故障;但一旦 bff 以 M2M 呼叫領域服務即全數 403。
建議採「轉傳使用者權杖」而非放行 M2M——gateway/BFF 係使用者之代理人,
改用 M2M 會使服務端失去使用者身分,使用者層級授權與稽核皆無從實作;
且現行設計已預留此路徑(使用者 token 之azp非cc-,不受 ACL 管制)。
⚠️ 若採此方向,CR-010 §9.5為二者補上的出站 M2M 組態即不應被使用,
須於SAD-005明訂「轉傳原則」以免日後誤用。
★ 影響範圍複核(2026-09-21 15:50,實測):frontend/vite.config.ts之 proxy 直連各領域服務、並未經過 gateway/bff,且其註解明載「正式環境應統一由 API Gateway(8100)出口」→ 目前影響為 0,但係已預定之地雷。api-gateway/bff-web兩模組實為空殼(src/main/java各僅 1 個Application類別,無路由、無 Feign client)。詳CR-011 §11「影響範圍」與「附帶實測」(本 ACL 極嚴格:矩陣未畫到之同步呼叫一律 403)。✅ 已執行:
SAD-005→ 1.3(新增 §3.4 身分轉傳原則);本規格新增 §7.11;程式新增OutboundMode/RelayingOutboundTokenProvider;gateway/bff 組態改為mode: token-relay並移除 client 憑證;common-spring85 → 91 測試 0 失敗。
11. 修訂記錄
| 版本 | 日期 | 說明 |
|---|---|---|
| 1.0 | 2026-09-19 | 初版。依 GOV-003 條件 9 於 W-3 動工前建立;揭露 3 項偏差(無閘道、角色不一致、401 無專屬碼) |
| 1.2 | 2026-09-20 | 推廣時發現 fail-open 缺陷並修復:新增 §7.6 依賴守門(cardcore.security.enabled=true 但 classpath 無 Spring Security 時啟動期失敗,實測:card-service 無 token 原回 200);新增 §7.7 單一服務導入步驟(依賴/組態/開關三段分離,組態已 17/17 就位);§10 更新實測覆蓋為 2/17,並新增第 10 項限制——前端無 token 來源,故常態不得全域啟用(否則 WebUI 8 頁全數 401) |
| 1.1 | 2026-09-19 | 實作完成並實機實測後回寫:①§7.3 補登 CC-PLT-4010/CC-PLT-4030(SDD-006 112 → 114 碼)並記錄 oauth2ResourceServer 蓋掉 entry point 致回應主體為空之實測修正;②§7.5 新增 Spring Boot 4 自動配置模組化陷阱(套件路徑改置,及改用 ObjectProvider<ObjectMapper> 去除順序依賴);③§9 補實測證據與 7 個自動化測試(common-spring 41 → 48);④§10 解除第 4 項限制、新增第 9/10 項(僅 party-service 實測、Keycloak 健康端點 404) |
| 1.3 | 2026-09-20 | WorkBuddy AI |
| 1.4 | 2026-09-20 | WorkBuddy AI |
| 1.5 | 2026-09-20 | WorkBuddy AI |
| 1.6 | 2026-09-20 | WorkBuddy AI |
| 1.7 | 2026-09-20 | WorkBuddy AI |
| 1.8 | 2026-09-20 | WorkBuddy AI |
| 1.9 | 2026-09-21 | 更正 §3 之錯誤陳述(原寫「並無 gateway/bff 模組」——實測二者位於 backend/platform/,確實存在),並補列其狀態表;因該句連同 §2 適用範圍使二模組被排除於認證規格外,連鎖導致 CR-010 v1.1 僅套用 17 個領域服務(已於 v1.2/v1.3 補正)。②§2 適用範圍改為「17 領域服務 + gateway/bff」。③§7.7 步驟 ② 改為 19/19 已就位(二模組依「預先就位」慣例補 issuer-uri)、③ 改為 19/19,並新增「gateway/bff 導入狀態」表(① 依賴仍未導入,已揭露)。④明載非本地環境刻意不在 application.yml 設 cardcore.security(依 §7.4 僅本地檔可顯式停用)→ 實測 19 份皆無該區段係設計如此,非缺漏,避免後人誤判為落差而補錯 |
| 1.10 | 2026-09-21 | WorkBuddy AI |
| 1.11 | 2026-09-21 | WorkBuddy AI |