跳至主要内容

ADR-034:統一 Server 對外位址契約

ADR 編號:034 決策者:Development Team 決策日期:2026-09-12 取代:局部取代 ADR-033 決策 1 對本 key 的 localhost 預設豁免 被取代:無 狀態:已接受(Accepted) 提案日期:2026-09-12 實作狀態(2026-09-12 快照):P1–P4 參考實作與本機驗收完成,P5 實作人審已通過;未 commit、未發布、未部署,不代表既有公開 artifact 已具備全部能力。

2026-10-13 編號承接:本文各節及歷史裁決中的「ADR-035」為 2026-09-12 的預留值。該編號已由報表決策使用;租戶網域現由 ADR-036:租戶自訂網域與入口租戶綁定 承接;2026-10-13 P0 設計審閱已通過,狀態為 Accepted、尚未實作。本 ADR 的已接受單一入口契約仍有效,設計接受不代表多租戶網域已交付。

背景與問題​

反向代理可以增加、替換或移除 Servlet context path。瀏覽器看到的對外基底路徑不一定等於 Server 的內部 context path。以 /gateway/ → /app-server/ 為例,瀏覽器需使用 /gateway/api/v1/auth,而目前 Refresh Cookie 的前綴拼接會得到 /gateway/app-server/api/v1/auth。

目前各功能對對外位址的認知分散:

功能現行來源影響
RefreshSessionCookiecookie-path-prefix + request context path替換/移除前綴無法表達,Cookie 不送回
SignedLinkAppServicesigned-link.link-base-url已能指定 public URL,但與其他功能分離
OidcAuthControllerrequest context path授權起始跳轉可能含內部路徑
OIDC 協定兩 Server 的 repo 設定未宣告 redirect-uri,未配置 forwarded-header 處理預設 callback 從內部 request 推算,T4 下會帶內部 /app-server,與對外 /gateway callback 不符;是 P1 優先驗證動機
NotificationService呼叫端 deepLink 或 NotificationDeepLinkProvider正常投遞仍可能攜帶錯誤連結;背景執行沒有 request
StaticSiteHelpersiteUrl 或 request 推算未明示時可能輸出內部位址
ProductController輸出 /api/...;既有 httpClient 將它視為 API base 相對路徑不能直接在現有相對值前補 /gateway,否則 client 會二次補前綴

SignedLinkService 簽署與驗證 JWT claims,不驗證接收請求的 Host/context path;此重構不變更 簽章格式。S3 presigned URL 使用獨立儲存 endpoint,不由 App Server 基底位址推導。

已同意的方向與方案比較​

方案評估
各 Feature 分別設定 URL/Cookie 前綴保留多個事實來源,無法確保部署一致性
全面依 request/Forwarded headers 推算背景通知沒有 request,仍需額外來源;需處理代理信任邊界
共用明示 public base URL(建議)瀏覽器流程與背景工作共用一份設定;各協定 adapter 負責接入

使用者已同意共用對外位址、框架先行、兩個 Server 參考實作一併遷移與兩階段人審。 以下契約已通過設計審閱,P1–P4 的參考實作與本機驗收已完成;P5 實作人審已於 2026-09-12 通過。發布狀態以框架及應用版本紀錄為準。

設定契約​

app:
public-base-url: https://example.com/gateway
  1. 本 ADR 第一版中 tenant 與 tenantless 均使用每部署單一 canonical Server public URL,包含 scheme、host、選用 port 與完整對外基底路徑。
  2. 對外路徑直接取代內部 context 的用途,不與 getContextPath() 相加。
  3. 根位址 https://example.com 與 https://example.com/ 等價;對外基底路徑為空,Cookie 根路徑輸出為 /。
  4. 只接受 HTTP(S) 絕對 URI;拒絕缺 host、userinfo、query、fragment、控制字元、反斜線、dot segments、重複分隔符與編碼後的路徑分隔符。尾端單一 / 可正規化。
  5. 路徑參數由組合 API 按 segment 編碼一次;query 由 query API 編碼。拒絕把任意絕對 URL 或 //host 當成應用相對端點,避免更換 origin 或逃離基底路徑。
  6. app.public-base-url 在非 DEV 環境必填;缺值/非法值在啟動期以英文訊息指出設定 key。僅明示 app.env=DEV 且 URL 缺值/空白時,可使用下節的本機預設;不從 request 猜測、不使用 classpath localhost 作部署 fallback。兩個 Server 的 env-config 標記保留 tiers(dev,test,prod),dev 可留空採用本機預設;測試由 fixture 明示。
  7. 租戶專屬網域需求已確認,延後至 ADR-035;第一版 resolver 不依 Host 或 tenant ID 切換位址。

與 ADR-033 的關係​

ADR-033 決策 1 將 localhost URL 視為已有值,未要求移除這類預設。本 ADR 對 app.public-base-url明定例外:即使 localhost 可供單一開發環境使用,也不提供 classpath fallback;非 DEV 環境都要明示。2026-09-15 修訂允許明確 DEV 使用下述本機預設。沿用 ADR-033 的啟動期驗證機制,但局部取代其對此設定的預設值豁免。 本 ADR accepted 時,必須同步回寫 ADR-033「被取代」欄及決策 1,限定例外只涵蓋本 ADR 的對外位址設定; 其他 localhost URL 的既有判準不變。P1 通過後已同步回寫 ADR-033 的局部例外。

2026-09-15 修訂:DEV 本機預設​

為降低本機開發設定成本,使用者確認以 app.env=DEV 作為此設定的開發環境判準:

  • 僅精確值 DEV 且 URL 未設定、空字串或全空白時啟用。未設環境、其他值或只有 Spring dev profile 均不啟用。
  • bootRun 已預設注入 app.env=DEV;IDE/直接啟動須自行明示。env-config 的 tier 註解不代表 runtime 環境。
  • 本機 URL 為 http://localhost:{server.port}{server.servlet.context-path};port 缺省 8080,context 缺省空字串;server.ssl.enabled=true 時使用 https。參考實作預設為 http://localhost:8080/app-server。
  • port 必須為固定的 1–65535;隨機 port(0)或停用 HTTP(-1)須明示 public URL,不猜測尚未綁定的埠號。
  • 明示 URL 優先,非法值仍拒絕啟動。DEV 的反向代理、tunnel 或非 localhost 入口須明示完整 URL;不讀 request/Forwarded headers。
  • 保留舊 cookie-path-prefix 及 link-base-url 的驗證;舊 URL 不作 fallback,與本機預設衝突仍須遷移。
  • 預設只存在於應用 skeleton 的 resolver 組裝,不寫回設定檔、不修改框架 primitive。啟動以英文 INFO 記錄採用的本機 URL。

此修訂僅放寬本 key;JWT、encryption root key 等必要機密仍依 ADR-033 準備。 既有下游需採納 config/PublicServerUrlConfig.java 的變更;此 skeleton 不由 /feature upgrade 自動下行。

分階段交付邊界​

使用者已確認同 Server 多租戶自訂網域需求;依設計裁決,該需求延後至 ADR-035 獨立設計, 先完成本 ADR P1–P4。原因是登入前 Host 信任、Tenant.slug 的唯一來源、SignedLink/OIDC 租戶綁定 與各租戶 IdP callback 都超出 URL 組合責任。本輪不新增 Host 租戶來源,不改現行租戶授權鏈。 租戶自訂網域現由 ADR-036(Accepted,P0 設計通過,未實作)承接,其驗收與登入前租戶信任契約由該 ADR 承接。 ADR-034 完成僅代表第一階段完成,整體多租戶網域需求仍須接續交付。

框架與應用責任​

框架新增不可變的對外位址值物件與組合能力(PublicServerUrl): 建構時驗證,提供對外基底路徑、應用端點路徑及完整 URI。能力不依賴 Servlet request, 不讀取 app.* 設定、不宣告業務 Controller。既有 LinkBuilder 的相對 href 契約保留, 由明確 adapter 消費其結果,不能全域改成絕對 URL 破壞原使用者。

框架保留位址選擇 SPI(PublicServerUrlResolver),接受顯式 PublicServerUrlContext 並回傳已驗證的不可變 PublicServerUrl。第一版 tenant/tenantless 都固定回傳單一設定值,上下文不影響選擇,也不承擔認證/授權。各 Feature adapter 只依賴 resolver,不直接讀設定或注入固定 URL;背景工作也經此入口。ADR-035 可 additive 提供租戶 resolver,無須重寫各 adapter。上下文型別第一版可表達無 request/既有可信業務上下文,不新增 Host 租戶信任語意。 不得把共用設定藏在 signed-link 或 oidc Feature 中;未 provision 它們時 auth 仍可使用。 環境設定產生流程集中維護單一位址,不由各 Feature 重複詢問。正式採用前依框架發版/升級流程 pin 公開 artifact; 本機候選依賴只作驗證,不當成 fleet 發布結果。

各功能接入​

功能預期行為(基底為 https://example.com/gateway)
Refresh CookiePath /gateway/api/v1/auth;write/clear 同源,保留 Secure、HttpOnly、SameSite 政策
OIDC 起始 redirectLocation /gateway/oauth2/authorization/{registrationId}
OIDC callback預設完整 URI https://example.com/gateway/login/oauth2/code/{registrationId},與 IdP 註冊一致
OIDC Session CookiePath /gateway,涵蓋 auth 起點、authorization 與 callback;根部署用 /
SignedLink consume URLhttps://example.com/gateway/api/v1/auth/link/consume?token=...
NotificationProvider 與自行給 deepLink 的呼叫端使用相同能力;背景簽發不取用 request
商品圖片/Server 下載保留既有 API base 相對路徑,由已認證 httpClient 套用 baseURL;不增加對外前綴、不改成絕對 URI
文件站保留文件掛載位置與明示站台 override;只有屬此 Server 的位址組合使用共用來源

前端 signed-link.redirect-base-url(含具名 app override)與 OIDC success-uri 仍代表前端服務; S3 endpoint、CDN URL 仍代表儲存服務。JWT 簽章機制與通知 Outbox/重試機制保留;本輪不增加租戶 claims 或入口核對,ADR-035 再獨立設計。

業務目標與交付連結的責任​

2026-09-12 使用者確認保留既有相對路徑契約。業務決定資源/頁面目標,不承擔部署 origin 或 context path。商品圖片與 Server 下載既有 /api/... 由 httpClient 接上 API baseURL, 不能在該值前再加 public path;本輪不為統一位址修改這些回應欄位或前端 URL 解析方式。 上傳 prepare 的相對 URL 與 S3 presigned URL 也保留原傳輸契約。

輸入/交付情境組合責任
前端業務目標(如 /products/{id}/edit)保持相對於前端 base;由前端路由或應用跳轉 adapter 消費
API 資源相對路徑保持相對於 API base;由 httpClient 消費,不當作瀏覽器根相對 href
SignedLink 寄出連結adapter 經 Server resolver 取得對外位址,再組合 consume 端點與 token
SignedLink consume/OIDC 完成跳轉應用 adapter 以明示前端 base 與相對業務目標組合;不使用 Server base 代替前端 base
Notification deepLink站內相對目標保留;已簽完整連結保留;若外部通道收到相對目標,交付 adapter 依目標屬前端或 API 選正確 base,不由通知引擎猜測

本輪統一需要對外位址的 adapter 來源,不要求所有業務連結一律轉換。P3 盤點既有通知呼叫端 與通道,確認相對值的消費方式;新增通知行為需求另行裁決,不悄悄重寫既有 Outbox 快照。

OIDC 與代理整合邊界​

先做最小整合驗證再擴大遷移:在現有 authorization resolver 接線中注入 public callback URI, 沿用 Spring Security 的 state/nonce/PKCE 與驗證機制;以 Spring Session 的實際 Cookie serializer 接點設定 Path。具體接點與容器差異在 P1 實測,不能只改 servlet session 屬性就宣告成功。

Controller、Security matcher 與容器 context path 保持內部路由語意。不以全域 request wrapper 偽造 context path。明示 public URL 對產生的外部位址具有權威;Forwarded/X-Forwarded-Prefix 不能再次加前綴。2026-09-12 核對兩個 Server 的 repo 設定與原始碼,目前沒有配置 forwarded-header 處理,registration 亦未明示 redirect-uri。此結論不涵蓋 repo 外部署檔/容器設定,P1 須驗證部署實況。由受信任代理清除/重設外來 headers; 不為此任務盲目啟用信任所有 header 的全域 filter。

若 registration 有自訂 redirect URI,啟動時檢核是否與此部署實際 callback 契約一致; 不靜默覆蓋不一致值。需特殊 callback 掛載的部署須在 Q2 裁決明示例外方案。

遷移與回復​

採兩階段交付,避免框架新增能力立即破壞舊應用:

  1. 框架 additive 發布:新增能力與測試,保留既有公開 API 行為;CHANGELOG 記錄新增能力。
  2. 參考實作切換:同一變更遷移全部消費端、env-config 與測試,公開必要的 Feature Changelog 遷移說明。

下表適用兩個 Server 的單一位址切換設定(Q3 已接受):

輸入切換版行為
新設定存在、舊設定不存在驗證新設定並啟動
新設定與舊 signed-link URL 等價新設定為唯一來源,提示淘汰舊 key
新設定與舊 signed-link URL 不同啟動失敗,要求明確遷移
舊 cookie-path-prefix 非空啟動失敗並提示移除;無法只靠 prefix 判定部署語意
舊 cookie-path-prefix 空白視為無 override,不參與計算
只有舊設定/新設定空白啟動失敗並提示新 key;部署前 migration 檢核可建議值,不自動猜測

切換時須移除 classpath 舊 URL 預設,避免把內建 localhost 當成使用者衝突;不能只新增新 key。 暫時舊 key 診斷的移除版本由發版審閱決定,不預填不存在的版號。公開 API 的淘汰另遵循至少兩個 minor 的 deprecation cycle。

部署切換前保存舊版 artifact 與設定,盤點 public URL、IdP callback、前端 API base、Cookie 名稱/Path/Domain 及仍有效通知。已寄出連結與 Outbox/站內通知快照不因改設定而改寫。舊 public 入口需保留至既有 link 有效期結束;保留 query 且不記錄 token。Outbox 超過 token 效期的投遞按既有到期規則處理,不冒稱重新簽發。

Path 改變時接受重新登入,不增加 legacy path 設定,也不承諾新應用能精準清除未知舊 Path。 對 T3/T4 等互不匹配的舊 refresh Path,驗證孤立 Cookie 不送往新端點、不被誤讀;舊 Cookie 自行到期。 但不能推論所有不同 Path 都不匹配:RFC 6265 §5.1.4 允許目錄前綴匹配,例如舊 Session Cookie Path / 仍會送到新 /gateway/...。部署預檢須辨識重疊情況,重疊時切換本應用 refresh cookie-name 及 Session cookie-name 作為本次部署設定,隔離舊名稱,不依賴 Cookie 順序。既有 session 撤銷/到期 沿用原管理機制;重新登入不等於舊 credential 已被撤銷,回復演練須涵蓋這點。 切換 refresh cookie-name 時,部署預檢必須同步前端 mock adapter、Vite 的 Cookie 名稱契約與 MockAuthSessionController 接線,確認兩端名稱一致;回復也須一起回復。 回復時以舊 artifact、舊設定、代理與 IdP 設定為一組;新發連結與新 Cookie 是否仍可用必須列入回復演練。

執行計畫​

階段交付物完成門檻
P0 設計人審本 ADR、設計簡報、Q1–Q5 裁決審閱方 verdict 通過,結論落回 ADR
P1 OIDC 垂直驗證共用能力最小實作、resolver/Session serializer 接線與代理測試替換前綴拓樸 T4 完成 OIDC 登入與 refresh,附證據;若契約需改先回設計審閱
P2 框架能力公開 API、URI 邊界測試、框架 CHANGELOG既有 LinkBuilder 契約回歸通過
P3 參考實作遷移兩 Server、auth/signed-link/OIDC/notification/資源與文件 adapters、env-configFeature 缺席/啟用組合皆可使用共用設定,兩模組測試通過
P4 參考實作整合驗收本機矩陣、Cookie/舊通知相容性證據、下游遷移與回復指南本機驗證通過、consumer 盤點與指南完成;實際部署/artifact 回滾屬下游採用責任
P5 程式碼人審change 簡報、實際 diff、測試與遷移證據審閱方通過後才 commit;發布與部署依各自流程

P3 驗證既有相對圖片/下載只套一次 API baseURL。對盤點中本來就接收完整 URL 的 isExternalUrl 呼叫端,核對其 API base 參數,測同源/跨源自家資源仍附 Authorization、外部 URL 不附;不因本 ADR 改相對路徑為絕對 URI或全域改寫 httpClient。 P3 一併盤點框架 mock auth Cookie、OpenAPI/Swagger、Actuator discovery、自動 redirect 與文件 Host 模組。 逐項標為「已啟用且需接入/已有正確來源/未啟用」,附檔案或配置證據;不能把未查核等同不受影響。 前端僅在 URL 消費契約確實需要調整時納入配套變更,不以改全域 base URL 掩蓋後端輸出錯誤。

參考實作與下游部署的驗收界線​

2026-09-12 使用者澄清:兩個 Server 是參考實作,目前沒有部署規劃。本輪 P4 以本機 可重現的協定/URL/Cookie 測試、既有授權與資源回歸、consumer 盤點及遷移指南為門檻。 實際環境切換、舊 artifact 回滾、正式 IdP/資料庫/多節點驗證由下游採用專案執行, 不要求為這次框架與參考實作交付建立部署環境。此為驗收範圍澄清,Q1–Q5 的設計與 相容性政策保持;不把本機相容性 fixture 記成已執行 artifact 回滾。

驗收矩陣​

ID對外基底路徑內部 contextRefresh Cookie Path
T1空空/api/v1/auth
T2/app-server/app-server/app-server/api/v1/auth
T3/gateway/app-server/app-server/gateway/app-server/api/v1/auth
T4/gateway/app-server/gateway/api/v1/auth
T5空/app-server/api/v1/auth
T6/edge/service空/edge/service/api/v1/auth

兩個 Server 均驗證 T1–T6 的 URL/Cookie 組合與代理請求。完整瀏覽器流程以 HTTPS 代理、 受控 IdP 與測試通知收件介面執行,至少覆蓋 T3/T4/T5:

  • 登入 → Cookie 實際送回 → refresh rotation → logout 清除;不只比對 Set-Cookie 字串。
  • OIDC 起點 → IdP → callback 保留原協定 session → exchange → refresh。
  • 無 request 的背景通知 → 點 consume → 前端 redirect → exchange → refresh。
  • 資源相對路徑以既有圖片/下載回歸、前端 API base 與 Authorization 測試、代理上傳流程驗證;文件 Host 另依明示 URL 來源盤點。實際部署的整頁資源可達性由下游驗收;原 401/403 授權案例仍成立。
  • 偽造 Host/Forwarded/X-Forwarded-Prefix 不改變 canonical URL;正常可信代理亦不二次拼接。
  • 缺設定、非法 URI、新舊衝突、Cookie Path/名稱隔離、舊通知入口有本機測試;版本回復提供下游演練步驟,本輪未執行舊 artifact。

單元測試另涵蓋 root/尾斜線、port、IPv6、Unicode segment、query escaping、dot segments、 編碼分隔符與 network-path reference。整合測試檢查 optional Feature 缺席不造成 bean 依賴失敗。 只做文檔的 P0 不宣稱上述測試已執行。

裁決​

第三輪設計審閱於 2026-09-12 通過,審閱方無保留接受 Q1–Q5。以下為持久決策紀錄。 收尾時已移除專案專屬待辦路徑,並保存持久設計裁決。

ID裁決理由日期
Q1租戶自訂網域需求保留,延後至 ADR-035;本輪兩 Server 固定單一 URL 並保留 resolver SPI先交付 URL 組合能力;登入前租戶信任、slug 唯一來源與租戶綁定需獨立設計2026-09-12
Q2接受顯式 adapter、保留內部路由及 callback 不一致拒絕啟動避免偽造 context path 或重複套用 forwarded 前綴;實際 OIDC/Session 接點由 P1 驗證2026-09-12
Q3接受 additive 框架能力、原子切換參考實作與舊 key 診斷表保留既有框架使用者相容性,同時避免兩個 Server 各 Feature 使用不同位址來源2026-09-12
Q4Path 改變重新登入;不加 legacy path;重疊以新 Cookie 名稱隔離;舊入口保留至 link 到期不承諾清除未知舊 Path;以名稱隔離、前端同步與回復演練處理部署相容性2026-09-12
Q5接受 PublicServerUrl/PublicServerUrlResolver 責任與命名、T4 OIDC/refresh 的 P1 門檻,再擴大至 T1–T6先證明關鍵代理拓樸,再遷移兩 Server;保留業務/API 相對路徑避免非必要 wire 變更2026-09-12

設計通過解鎖 P1。2026-09-12 P1 完成後將本 ADR 改為 Accepted,並同步回寫 ADR-033 的局部取代關係。P1 未改動已接受契約;程式碼交付仍須 P5 change 人審。

P1 驗證證據(2026-09-12)​

兩個參考 Server 都以本機 composite build 接入候選框架,透過 publicUrlEvidenceTest 啟動真實 Spring Boot/Spring Security/H2 JDBC Session,再由 Chromium 經 HTTPS 代理 執行 T4。代理只把 /gateway/ 替換成 /app-server/;未重寫回應的 Location/Set-Cookie, 未傳遞 forwarded headers。TLS 使用測試即時產生的憑證,Browser fixture 明示接受該測試憑證。

驗證結果
tenant PublicUrlOidcProxyIT1 case,0 failure;實際 OIDC → callback → exchange → refresh rotation → logout → 無 Cookie 401
tenantless PublicUrlOidcProxyIT1 case,0 failure;同一流程通過
Session斷言實際 bean 為 JdbcIndexedSessionRepository/DefaultCookieSerializer;callback 請求確實帶 Session Cookie
CookieChromium 保存 Session Path /gateway、Refresh Path /gateway/api/v1/auth;Secure/HttpOnly 生效,Session SameSite=Lax;refresh credential 輪替且 logout 後消失
IdP受控 HTTP IdP 提供 discovery/JWKS/RS256 ID token,檢查 public callback 與 S256 PKCE;Spring 驗證 state/nonce/ID token
共用能力PublicServerUrlTest 34 cases、既有 LinkBuilderTest 24 cases 通過
兩 Server 定向回歸每模組 41 cases 通過:設定缺值/舊 prefix、callback 衝突、Cookie、OIDC context、refresh 保權限與 401/403 授權矩陣
完整 buildtenant 754 tests(0 failure、3 skipped);tenantless 491 tests(0 failure、1 skipped),兩者 build 成功;瀏覽器 case 由獨立 evidence task 執行
文件appfuse-docs npm run build 成功;llms.txt 0 missing/0 stale

P1 確認的實際接點:

  • Boot 4 必須使用 spring-boot-starter-session-jdbc,原先只引入 Spring Session JDBC library 不足以啟用 Boot Session auto-configuration。使用 DefaultCookieSerializerCustomizer 設 Path。
  • Session 存取使用具名 springSessionTransactionOperations,內部為獨立 JDBC transaction, REQUIRES_NEW;不借用 JPA manager。否則 tenant 的 deferred anonymous authentication 讀 Session → JPA 建立 EntityManager → tenant resolver 讀 SecurityContext → 再讀 Session,會遞迴。 該 bean defaultCandidate=false,讓一般業務仍取得原有 JPA TransactionTemplate。 後續實作審閱發現:只分離 manager 仍可能沿用同一 DataSource 綁定的 JPA ConnectionHolder, 造成 Session 提交業務連線。最終接線改用 SessionInfrastructureConfig 的 @SpringSessionDataSource DelegatingDataSource,同一 pool、獨立 TSM key,repository 與 manager 同時使用 wrapper。此修正保留 JPA rollback 語意,並不隨 optional OIDC 移除。
  • OIDC authorization request customizer 僅設定 public callback,保留原 state/nonce/PKCE; 內部 Controller/Security matcher 與 Servlet context 保持原路由語意。

重現方式(各 Server 目錄):

./gradlew publicUrlEvidenceTest
./gradlew test --tests '*PublicServerUrlConfigTest' --tests '*RefreshSessionCookieTest' \
--tests '*OidcAuthControllerTest' --tests '*OidcSecurityConfigurerTest' \
--tests '*OidcProtocolContextRepositoryTest' --tests '*RefreshTokenAuthorityIT' \
--tests '*SecurityAuthorizationMatrixIT'

Browser fixture 需要 Node、Playwright 與已安裝 Chromium。以 ./gradlew publicUrlEvidenceTest -Dpublic-url.playwright-module=/absolute/path/node_modules/playwright 指定安裝位置;來源 workspace 有本機預設,下游可覆寫,不要求特定前端模組名稱。 Java 測試與 Browser 腳本分別在兩 Server 的 src/test/java/io/leandev/app/security/oidclogin/PublicUrlOidcProxyIT.java、 src/test/resources/public-url/oidc-browser.cjs;報告在各模組 build/reports/tests/publicUrlEvidenceTest/index.html 與 build/test-results/publicUrlEvidenceTest/。 一般 test 明示略過瀏覽器 case,必須另跑 evidence task 才能宣稱完成。

界線:外部 identity → 已存在 admin 的 mapping 使用明示 test bean;tenant seed 的 admin 綁定 default-tenant,不是 platform superadmin,不以 X-Tenant 代替帳號綁定。未替換 OIDC 協定、 exchange、refresh、JWT 或持久 Session;本測試不代表完整外部身分映射政策已驗收。 未驗證 WAR/外部 Tomcat、跨節點、真實 IdP、外部資料庫、T1–T3/T5–T6、舊通知與回復演練。 P3 的 SignedLink/notification/文件消費端及新舊 URL 診斷尚未切換,P1 工作樹不得當成 已完成的原子遷移部署;既有業務/API 相對路徑契約保持。

P2/P3 實作與 P4 驗證進度(2026-09-12)​

P2/P3 的工作區實作已完成,尚未 commit/發布。依使用者澄清的參考實作範圍, P4 本機驗收完成,P5 change 人審已通過(2026-09-12)。以下是 P1 之後的 新增證據,P1 節末所列未驗證項目為當時快照。

實際接入盤點​

項目狀態與檔案證據
共用 URL/舊設定診斷兩 Server config/PublicServerUrlConfig;純 ApplicationContextRunner 不載入 optional Feature 仍可啟動;缺值/非法值/衝突拒絕、相同舊 URL 警告
Refreshcontroller/auth/RefreshSessionCookie 從 resolver 取得 /api/v1/auth Path
OIDC/Spring SessionOidcAuthController、OidcSecurityConfigurer 與 OIDC feature 的 SessionInfrastructureConfig;callback 模板展開同一 canonical URL,保留協定與 JPA 接線
SignedLink兩 Server service/signedlink/SignedLinkAppService 的 consume URL 只從 resolver 產生;移除舊 localhost 預設
通知tenant 的 LowStockSignedLinkDeepLinkProvider 與兩 Server 的 ImpersonationConsentService 委派 SignedLink;tenantless 沒有 Sales provider,不新增不存在的業務接線;Notification 引擎原樣保存 deepLink
Actuator兩 Server config/PublicActuatorLinksConfig 覆寫 Boot 的 mapping bean,僅替換 EndpointLinksResolver 基底;保留供應者、media types、CORS、discovery 與集中授權。獨立 management port 仍由既有安全設定拒絕
圖片/下載/上傳 URL保留業務的 /api/...;前端 httpClient 套一次 API base。appfuse-web/lib/components/data-input/media-input/sortable-thumbnail.tsx 漏傳 API base,已修正跨源自家完整 URL 的分類
其他完整 URL 消費端AuthImage、MediaViewer、PDF viewer、file-download 已傳 API base;相對資源與外部 URL 的 Authorization 分流維持
OpenAPI/Swagger兩 Server 源碼與 build 未啟用 runtime Springdoc/Swagger;無此 consumer 需要 adapter
文件 Host獨立文件 Host 的 StaticSiteController 已使用明示 docs.siteUrl/docs.basename;不屬兩個 API Server 的 canonical base。未改成共用 API Server URL
Mock auth兩 Server 未組裝 MockAuthSessionController;獨立前端 Host 的 mockauth/MockAuthConfiguration 在 app.msw.enabled=true 才組裝,預設 false。Vite plugin 預設 APP_SERVER_REFRESH,Cookie 改名仍須依部署指南同步接線
其他 redirectOIDC success-uri、SignedLink redirect-base-url 仍是獨立前端位址;Controller/Security matcher 的內部路徑不加 public prefix

本機測試證據​

框架完整 build 已通過:1,007 tests,0 failure、9 skipped。之後針對 Cookie Path 分號 邊界新增一例,PublicServerUrlTest 共 50 cases 與既有 LinkBuilderTest 24 cases 定向重跑。 框架檢查含 scheme/host/default port/IPv6、Unicode、percent encoding、query injection、 traversal、null 與不可變契約;PublicServerUrl 不接受無法直接用於 Cookie Path 的 raw 分號。

前端 82 cases 通過(url.spec.ts、http-client.spec.ts、media-input-download.test.tsx), npm run build 通過。MSW 觀察真正 httpClient 送出的 URL 與 Authorization,驗證相對路徑 在 /gateway 與 /edge/service 只套一次前綴;自家完整 API URL 與外部 CDN 分流。

兩 Server 的 publicUrlEvidenceTest 皆使用六種真實 HTTPS 代理拓樸,明示 server.forward-headers-strategy=none;沒有宣稱驗證 framework/native header 改寫模式。測試通知通道只在 SMTP 末端替換為受控 inbox;NotificationService、資料庫 Outbox、dispatcher、簽章、consume、 exchange/refresh 與 Spring Session 保持實際組裝。流程涵蓋:

  • T1–T6 OIDC → callback → exchange → refresh rotation → logout → 401。
  • callback 預設模板、明示完整 callback、組合式 Spring 標準變數模板。
  • 背景產生連結 → NotificationService → Outbox → 寄出 → 由信件 HTML 取出連結 → consume/exchange/refresh。
  • 模擬舊 URL 快照原樣落庫/遞送,保留 /retired-app-server 入口後仍能 consume/exchange。 這是既有 signed-link wire 格式與舊入口相容性測試,沒有執行舊版 artifact。
  • 偽造 Host 入口探測及 Forwarded/X-Forwarded-* 不改 callback/Actuator canonical links;代理不重寫回應。
  • 重疊 root 舊 Cookie 以新名稱隔離,logout 不冒稱清除未知舊名稱;T3/T4 孤立 refresh Path 不送往新端點。
  • 真實 staging prepare/PUT/metadata 經代理可達,回傳的相對 URL 只套一次 API base。

P4 初次兩 Server 完整 build 通過:tenant 756 tests(0 failure、3 skipped)、tenantless 493 tests (0 failure、1 skipped);瀏覽器矩陣由獨立 task 各跑 6 cases,0 failure。build 包含既有 401/403 與 Actuator 自訂 base path/path-mapping 的授權回歸。框架 Javadoc 與文件完整 npm run build(含 Storybook/Javadoc 同步、索引與匯入檢查)通過,最後文檔內容另經 build:docs-only 驗證。 瀏覽器 fixture 是測試協定與路徑的受控客戶端,沒有載入正式前端應用的所有圖片/下載頁面。

實作審閱修正後再次執行 ./gradlew publicUrlEvidenceTest build:tenant 766 tests (0 failure、3 skipped)、tenantless 503 tests(0 failure、1 skipped),兩邊瀏覽器矩陣 各 6 cases 通過。一般 JVM suite 已包含四種 callback 正向模板、實際 resolver 的 state/nonce/PKCE、Actuator canonical href/自訂 path mapping,以及不載入 OIDC 設定時的 Session skeleton bean 與 Cookie Path 測試。

SessionTransactionIsolationIT 先在 JPA 交易寫入資料,再經實際 Session filter 首次 讀取並更新 JDBC Session,最後回滾 JPA 交易;斷言業務寫入回滾且 Session 更新仍保存。 另以 tenant 實作暫時移除 DelegatingDataSource 包裝重跑,確認此 rollback 斷言失敗 (業務寫入被提前提交),恢復隔離後完整回歸通過。兩者仍共用連線池;JPA 持有連線時首次存取 Session 需要第二條連線,須依並行交易量 預留容量,避免外層交易耗盡池後等待 Session 連線。這補足僅用 REQUIRES_NEW 未隔離 DataSource 資源鍵的缺口。修正後前端 82 tests 與文件 build:docs-only 亦通過。

2026-09-15 修訂:HTTP Session 隨 OIDC feature 安裝​

上述 P1–P5 的測試數與 skeleton 描述保留為當時證據;目前安裝責任改由實際 consumer 決定。 現有 feature 中只有 oidc-login 需要 HTTP Session 與 Spring Security OAuth2 Client; auth 的 refresh token 使用獨立 JPA 紀錄,郵件 OAuth2 也不使用 OAuth2 Client starter。 因此兩個 starter 移至 gradle/features/oidc-login.gradle.kts,Session configuration 移至 feature/oidclogin/,兩者及專屬測試由 OIDC catalog 擁有。未選 OIDC 的 scaffold/下游不帶入。 保留 DataSource wrapper、獨立 resource key、交易隔離與 canonical Cookie Path;這次只改安裝歸屬。 營運期停用 OIDC 登入不等於卸載 feature,已安裝的 Session 接線仍存在。

既有無 OIDC 下游移除多帶入的相依與 Session skeleton;若應用自行使用 HTTP Session, 由應用保留其依賴與接線。舊 SQL migration 不修改、不刪表。新 OIDC 安裝預設 spring.session.jdbc.initialize-schema=always,啟動時以 Spring SQL 初始化兩張 Session 表; 資料庫帳號須有 DDL 權限。沿用 continue-on-error=true,重啟遇到既有表時保留資料, 但不會自動升級 schema,也不保證被忽略的 DDL 錯誤無害。 若 Session schema 由 Flyway/DBA 管理,於外部 .properties 明示 never,沿用原有 migration 序列。此修訂取代先前要求所有下游保留 Session skeleton、外部 DB 一律先手動建表的指示。

下游採用時的部署驗收責任(本輪不適用)​

  • 確認回滾測試環境及舊 artifact/外部設定版本,以 artifact、設定、代理、IdP 為一組 實際切換後回復,驗證新/舊連結與 credential 撤銷/到期狀態。
  • 在該環境檢查正式圖片/下載/文件入口與前端 mock/Vite/Host Cookie 名稱(若啟用)。 本機已具備 API 路徑、上傳、前端資源分類及既有圖片回應測試;未冒稱完成實際部署頁面驗收。
  • WAR/外部 Tomcat、真實 IdP、外部 DB Session schema 與多節點行為需依實際部署模式記錄。

部署與回復指南 已提供設定表、原子遷移步驟、舊入口 保留與 Cookie 名稱契約。上述事項由有部署計畫的下游專案依實際環境執行,不作為本輪 參考實作 P4/P5 的前置條件,也不代表這些部署情境已經通過驗證。 本機未安裝 /security-review,P5 預檢須明示此限制;本變更以 ADR 為單位、沒有對應 US, 不可把一般自行檢查冒稱為已執行 US 專用 /code-review。

參考依據​

變更歷史​

日期內容
2026-10-13ADR-036 第 3 輪 P0 設計通過並 close;更新承接狀態,single 與既有發布行為保留,多租戶網域尚未實作
2026-10-13新增 ADR-036 承接說明;保留歷史 ADR-035 預留編號及已接受契約,租戶網域草稿尚未實作
2026-09-12建立設計與執行計畫草稿;待設計 peer review,尚未實作
2026-09-12第一輪意見修訂:ADR-033 局部例外、OIDC 現況、Cookie 重新登入策略、圖片絕對 URI、dev tier 與格式;待第二輪審閱
2026-09-12使用者確認同 Server 多租戶網域,撤回單一入口限制;補受控目錄、可信入口、背景租戶傳遞、租戶綁定及 M1–M6,納入第二輪
2026-09-12第二輪處置:租戶網域獨立至 ADR-035,本輪固定 resolver;補前端 Cookie 同步與 isExternalUrl 呼叫端盤點
2026-09-12使用者確認保留業務/API 相對路徑,撤回圖片下載絕對 URI 提案;明確區分業務目標、Server URL 與前端跳轉組合責任
2026-09-12第三輪設計審閱通過;完成來源清理並保存 Q1–Q5 裁決;維持 Proposed 待 P1 證據
2026-09-12P1:兩 Server 的 HTTPS T4 Chromium OIDC/refresh/logout 通過;確認 Spring Session JDBC 與 transaction 接點,ADR 改 Accepted 並回寫 ADR-033;完整遷移與程式碼人審仍待後續階段
2026-09-12P2/P3:共用 URL API、兩 Server consumer/舊設定遷移、Actuator 與前端 API base 修正;補 P4 本機矩陣與部署指南。P4 版本回滾及實際環境驗收待補,P5 尚未啟動
2026-09-12使用者澄清兩 Server 為無部署規劃的參考實作;P4 以本機驗證、盤點與指南結案,實際部署/artifact 回滾列為下游採用責任;進入 P5 實作人審
2026-09-12實作人審第一輪處置:Session DataSource/TSM 隔離上移 skeleton、補 JVM callback/Actuator/rollback 測試、改善設定診斷與下游指南;瀏覽器明示租戶與 forwarded-strategy 證據界線
2026-09-12P5 實作人審第三輪通過,21 條意見全數處置、無 declined;結論已落於本 ADR、遷移指南、Feature Changelog 與後續待辦。Server/文件重構與既有 MediaInput API base 缺口修正分為兩個 commit,後者使用 fix(appfuse-web),維持原始請求 URL。人審通過不等同已發布或部署。
2026-09-15依使用者確認,OAuth2 Client/JDBC Session 相依、接線與專屬測試改由 optional OIDC feature 擁有;無 OIDC 的下游移除多帶入的 skeleton,保留交易隔離設計及既有 migration。
2026-09-15使用者確認 OIDC 安裝後預設自動初始化 JDBC Session schema(always);Flyway/DBA 管理者以外部 never 覆寫,保留既有 migration。