代理與模擬(Acting)使用指南
Package:
io.leandev.appfuse.security.auth.*核心類別:ActingContext、ActingClaims、JwtTokenProvider決策背景:ADR-013: 代理與模擬——ActingContext 框架能力
框架以最薄的機制面支援「以他人身份/權限操作系統」的兩種能力——代理(Delegation)與模擬(Impersonation)。框架交付 token 簽發擴充、JWT claim wire 契約、ActingContext 讀取工具與 request-level 稽核事件能力;啟用時機、閘門、授權與啟用記錄、資料表、instance 級過濾一律留給應用層(由 app-tenant-server / app-office 出參考實作)。
本指南分兩半:前半是框架設計(你在任何 appfuse 專案都能用的機制),後半是參考實作走查(花店 app-office 如何在框架機制上落地一套完整政策)。
一、兩種能力的語意差異
代理與模擬常被混為一談,但語意根本不同——這組差異是整個設計的核心:
| 面向 | 代理 Delegation | 模擬 Impersonation |
|---|---|---|
| 一句話 | 登入者借用他人權限操作 | 登入者取得他人身份操作 |
| 實際執行者(對系統而言) | 仍是登入者 A | 是被模擬者 B |
身份(JWT sub) | 維持登入者 A | 切成被模擬者 B |
權限(JWT auth) | A ∪ 授權人的權限 | = B 的權限 |
稽核 @LastModifiedBy | 記 A | 記 B |
| 對象數 | 多個(0..n) | 單一 |
| 跨租戶 | 不跨(同租戶) | 可跨(管理/客服超能力) |
| 額外 claim | grantors(授權人 subject 集合) | actor(真實操作者 subject) |
| 前端提示 | 低調指示器(身份未變) | 醒目橫幅(身份被換掉) |
判斷句:identity 被換掉 → 模擬(橫幅);只是多借到權限、身份沒變 → 代理(指示器)。
兩者正交、可同時發生:代理與模擬不是二選一——它們是兩個獨立維度,可共存於同一 token(如「A 模擬 B,而 B 本身被 C 指派代理」)。框架不設
act_mode這類模式判別器,模擬性/代理性由actor/grantorsclaim 的存在性各自獨立判定(見 §3、§4 的正交模型)。
兩者不互相取代。模擬能跨租戶、換身份,看似「更強」,但它抹掉了「真正是誰在操作」——代理保留登入者身份、只補權限,稽核直指真人。用途不同,並存。
二、設計哲學:框架只交付四件
appfuse-server 對「代理/模擬」刻意保持薄機制面,理由見 ADR-013:授權模型(誰可對誰、時限、scope)與持久化因應用而異,框架若硬塞資料表或端點會限制適應性,也與「框架只擁有 tenant 隔離、resource 級留應用層」的既有邊界矛盾。
因此框架只提供:
| # | 交付物 | 類別 | 職責 |
|---|---|---|---|
| 1 | Token 簽發擴充 | JwtTokenProvider | 正確地簽出代理/模擬 token(claim 對稱) |
| 2 | Claim wire 契約 | ActingClaims | 簽發端 ↔ 讀取端唯一約定的 claim 名與組裝 |
| 3 | 讀取工具 | ActingContext | 從當前 JWT 算出 {subject, grantors[], actor} |
| 4 | 稽核事件能力 | ActingAuditInterceptor | 代行請求完成後發布 ACTING_ACCESS |
框架不提供:授權表、啟用記錄表、enter/exit 端點、閘門 authority、URL 掛載範圍、持久稽核 sink。這些一律由應用層設計。
2.1 Feature 導入、預設與邊界
acting 是 optional Feature,本身不宣告 prerequisite;delegation 與 impersonation
需要它時,由 feature dependency 自動帶入。其組成刻意分為:
- Feature core:
ActingConfig提供可覆寫的ActingAuditInterceptor預設 bean; 使用者宣告同型 bean 時,@ConditionalOnMissingBean讓位。 - Reference domain:
ActingController把 stable subject 經PrincipalDirectory即時映射成 username,並以/api/v1/auth/acting提供 UI 顯示契約;ActingWebConfig選擇把 core interceptor 掛在/api/**。 - Catalog seam:
acting沒有 catalog seam,也沒有 property、schema 或 migration。 可覆寫 bean 是 Spring extension point;URL、response 與掛載範圍則是 app-owned reference policy,不是 framework seam。
若產品要客製,可替換 ActingAuditInterceptor bean,或直接 retarget reference
controller/config;不要在 reference config 重新建一份 framework bean。若要移除,須先移除
依賴它的 delegation/impersonation,再移除 acting reference domain 與 core。
tenant 與 tenantless variant 的 acting core、reference source 與 catalog 宣告皆為
零差異;租戶差異由 token 的 tenantId 與上層 delegation/impersonation 政策承擔。
另外,interceptor 只會發布事件;需要合規等級的可追溯性時,應另 provision audit
及持久 AuditEventRepository,否則事件不保證跨程序生命週期留存。
三、框架 API
3.1 Token 簽發擴充(JwtTokenProvider)
框架在既有的 generateToken(...) 之上加兩個代行專用簽發方法。載入被代理/被模擬者、選哪些 ROLE、決定是否允許皆由應用層負責——框架只負責正確地簽。
/// 產生代理 token:sub 維持登入者、auth 取 subject 的權限
/// (呼叫端應先把「借用 ROLE 展開的權限」併入 subject.getAuthorities())、帶 grantors claim。
public String generateDelegationToken(UserDetails subject, List<String> grantors,
Map<String, Object> extraClaims);
/// 產生模擬 token:sub/auth 換成被模擬者、帶 actor claim。
/// 被模擬者的 tenantId 由呼叫端於 extraClaims 帶入(模擬可跨租戶)。
/// 要同時具代理維(被模擬者本身被指派代理),於 extraClaims 併入 ActingClaims.delegation(...) 即可。
public String generateImpersonationToken(UserDetails impersonatedSubject, String actor,
Map<String, Object> extraClaims);
- 代理:
subject傳登入者,但其getAuthorities()應已是「自身 ∪ 借用權限」的合併結果(合併在應用層做)。grantors至少一筆。 - 模擬:
impersonatedSubject傳被模擬者,其 stable subject/authorities 成為 token 的sub/auth;actor是真實操作者的 stable subject(供結束模擬與稽核)。 - 兩維可組合:
grantors與actor是獨立 claim、無 key 衝突——要簽「模擬 B 且帶 B 的代理維」,把ActingClaims.delegation([C])merge 進模擬簽發的extraClaims即可。 - 兩者的
sessionId經extraClaims帶入,沿用既有黑名單撤銷機制。
3.2 Claim wire 契約(ActingClaims)
代理授權人、模擬者與有效身份一律以不可變 stable subject 識別,對齊 JWT
sub。AuthPrincipal.subject() 是簽發來源;username、email、顯示名等可變欄位不得進入
framework claim contract。需要顯示名稱時,由應用層透過 PrincipalDirectory 映射。
兩個正交 claim,無模式判別器:模擬性由 actor 存在表達、代理性由 grantors 非空表達,兩者獨立、可並存。
| claim | 常數 | 代理 | 模擬 | 說明 |
|---|---|---|---|---|
sub | — | 登入者 A | 被模擬者 B | 既有;auditor 讀此 |
auth | — | A ∪ 借用權限 | B 的權限 | 既有;converter 還原 authorities |
tenantId | — | A 的 tenant | B 的 tenant | 既有;TenantContext 解析 |
sessionId | CLAIM_SESSION_ID | 有 | 有 | 既有;黑名單撤銷 |
actingPolicy | CLAIM_ACTING_POLICY | 選用 | 有 | Refresh Session 綁定的應用政策識別 |
grantors | CLAIM_GRANTORS | [stable subject, …] | —(除非共存) | 代理維,非空即代表代理性(0..n) |
actor | CLAIM_ACTOR | —(除非共存) | stable subject | 模擬維,存在即代表模擬性 |
為何沒有 act_mode:模式判別交由 grantors/actor 的存在性承擔——act_mode 承載零額外資訊,且單一值天生表達不了「同時模擬又代理」。安全性不靠它把關:token 完整性由 JWT 簽章保證,無法在不重簽(必經 JwtTokenProvider)下注入假 claim。
ActingClaims 提供組裝工具,供簽發端組出代行 claim(generateDelegationToken / generateImpersonationToken 內部即呼叫):
ActingClaims.delegation(List.of("account-a", "account-b")); // {grantors=[account-a, account-b]}
ActingClaims.impersonation("account-c"); // {actor=account-c}
// 無 act_mode key → 兩者可 merge:{actor=account-c, grantors=[account-a]}
Refresh Credential 是不透明 secret,不承載 claim。Refresh Session family 保存
subject/actorSubject/SessionPolicyBinding;每次 refresh 重載 subject、actor 與 delegation,重驗 policy 絕對效期、actor 必要 authority 及應用提供的SessionEligibilityPolicy,再重鑄grantors/actor/actingPolicy。政策拒絕會撤銷整個 family。
3.3 讀取工具(ActingContext)
ActingContext 是讀取端唯一入口——把當前 JWT 的代行 claim 在框架邏輯下算成結構化脈絡,供應用層寫啟用/稽核記錄、做 instance 級過濾。
public interface ActingContext {
String getSubject(); // 有效身份 stable subject(代理=A、模擬=B,即 JWT sub)
List<String> getGrantors(); // 代理授權人 stable subject 全集(0..n;無代理回空)
String getGrantor(); // 單數便利:>1 丟 IllegalStateException、0 回 null
String getActor(); // 模擬者 stable subject;非模擬時退化回 subject
String getSessionId(); // 目前 access/refresh family 的 session id
String getActingPolicy(); // 目前 family 綁定的應用政策識別
boolean isImpersonating(); // 帶 actor claim
boolean isDelegating(); // 帶非空 grantors;可與 isImpersonating() 同時為 true
}
取得脈絡(三個靜態工廠):
// 最常用:從當前 SecurityContext 讀
ActingContext acting = ActingContext.current();
// 從 resource-server 的 Jwt principal
ActingContext acting = ActingContext.of(jwt);
// 從原始 claim map(測試或非 resource-server 情境)
ActingContext acting = ActingContext.ofClaims(claims);
語意細節(實作於 DefaultActingContext):
isImpersonating()/isDelegating()是兩個獨立述詞(各由actor/grantors的存在性判定),可同時為 true(共存情境);無單一getMode()。getGrantor()是給「只支援單一授權人」的應用的便利存取:恰好一筆回該筆、零筆回null、多筆卻以單數存取則丟IllegalStateException(不允許把多筆當一筆讀)。getActor()在模擬時回actorclaim;其餘情況退化回subject自身——因為「真實操作者恆為當前 principal」,代理/一般 session 下操作者就是登入者本人。
目前狀態欄為何自動正確:
AuditorAware只讀authentication.getName()(即sub)。代理維持sub=登入者、模擬切sub=被模擬者,於是@LastModifiedBy免改即正確。ActingContext是旁路資訊、不改既有 auditor 預設;需要 actor/grantors 與 Entity 版本時,另選配 Entity history。
四、稽核與資料隔離的互動
稽核:三層獨立互補
JPA 目前狀態欄(@LastModifiedBy)只記有效身份(sub)。要看見 acting 維度,可依查詢目的獨立選配 Entity history 與 request-level audit event:
① 資料列級 — Spring Data JPA Auditing(AuditableBase)
@CreatedBy/@LastModifiedBy/@CreatedDate/@LastModifiedDate+AuditorAware(讀SecurityContextHolder…getName()=sub)。- 每次 persist/update 把「誰(=
sub)、何時」蓋到資料列。單一@LastModifiedBy不承載 actor/grantors;既有 Entity 與AuditableBase不需修改。
② Entity changeset 級 — optional Entity history
- 應用安裝
entity-auditfeature,並只對需要歷程的 Entity 加@Audited。 - Envers
_AUD表保存 Entity 版本;application-ownedaudit_revision保存同一 transaction 的subject/actor/grantors/principalKind/requestId/sessionId/source,tenant variant 另存 tenant。 - impersonation/delegation 都不是必要相依;沒有對應 claim 時自然退化為
subject = actor、grantors = []。詳見 Entity 歷程稽核。
③ 存取級 — request-level acting 稽核 hook + 持久 AuditEventRepository
- 關鍵洞察:JWT 於每次認證請求都自帶完整代行脈絡(
sub/actor/grantors),Spring Security 每請求處理它 →ActingContext.current()隨時可還原。故 acting 稽核的正確落點是存取層,不需 session 表。 - jar 的
ActingAuditInterceptorcapability 在代行脈絡下完成的每個請求,發一筆ACTING_ACCESSAuditEvent(principal=真實操作者,data 帶subject/actor/grantors/actingModes/sessionId/actingPolicy/method/uri/status);actingFeature 提供可覆寫的預設 bean, app-tenant-server reference domain 才選擇掛在/api/**。轉場另發IMPERSONATION_STARTED/ENDED、DELEGATION_APPLIED。這些事件的 principal 與 data 身份欄一律使用 stable subject;DELEGATION_APPLIED的發布方法必須位於可寫交易, 才能與持久 sink 在同一交易內真正落表。 - 參考實作把這些
AuditEvent經PersistentAuditEventRepository(實作 Spring Boot Actuator 的AuditEventRepository)持久落audit_event表,/actuator/auditevents可查詢;Spring Security 的登入/授權稽核事件也共用此 sink。- 持久 sink 是框架 capability(
io.leandev.appfuse.audit,opt-in,ADR-013 R1);auditFeature 本身以 in-memory sink 作零資料表預設。需要持久稽核的應用才由 reference domain 宣告 entity、repository 與AuditPersistenceConfig,後者透過 repository provider 讓 Feature 改選持久 sink,過程只產生一個AuditEventRepositorybean。acting 的事件解算同樣是 capability;哪些 URL 納入稽核仍是應用 reference policy。
- 持久 sink 是框架 capability(
- 不再有 acting 專屬 session 表——
ImpersonationSession/DelegationSession已移除(JWT 每請求自帶脈絡,session 表對「軌跡」是冗餘)。
如何選:只需要目前狀態就用
AuditableBase;要查某 Entity 每一版與 changeset actor 就加 Entity history;要查未改 Entity 的存取、登入與 HTTP 結果則用AuditEvent。三者可單獨或一起啟用,不必把 acting 欄位塞進所有業務 Entity。
資料隔離:跨租戶不對稱
| 隔離模式 | 代理 | 模擬 |
|---|---|---|
tenant | 同租戶;資料可見性本即整租戶,代理是「輕版」純權限合併 | 換 tenantId claim → TenantContext + Hibernate @Filter 自動跟隨 |
ownership | 應用層讀 getGrantors() 把授權人 subject 集合折入擁有權過濾 | 隨 B 身份自動成立 |
- 模擬幾乎免費:既有 tenant/ownership 機制都 key 在 principal 身份,模擬換掉
sub/auth/tenantId即自動跟隨,框架不改邏輯。 - 代理不跨租戶:技術上
TenantContext/Hibernate filter 為單租戶,且實務罕見;代理 token 維持登入者tenantId。
五、參考實作走查(花店 app-office)
以下為 app-tenant-server / app-office 的參考實作,屬 @reference-surface(見 m-reference-code.md),下游 retarget 時替換為自己的政策。框架碼不動,只換應用層政策。
5.1 端點總覽
| 方法 | 路徑 | 閘門 | 職責 | 歸屬 |
|---|---|---|---|---|
POST | /api/v1/auth/impersonation | hasAuthority('impersonation:enter') | 進入一般模擬 | Impersonation |
POST | /api/v1/auth/impersonation/break-glass | hasAuthority('impersonation:break-glass') | MFA step-up 緊急模擬 | Impersonation |
POST | /api/v1/auth/impersonation/break-glass/cases/{caseNumber}/close | hasAuthority('impersonation:break-glass') | 結案並撤銷該案件全部 family | Impersonation |
POST | /api/v1/auth/impersonation/exit | isAuthenticated() | 結束模擬 | Impersonation |
GET | /api/v1/auth/impersonation/candidates | hasAuthority('impersonation:enter') | 可模擬對象名單 | Impersonation |
GET | /api/v1/auth/delegation/grants?activeOnly=true&page=0&size=50 | isAuthenticated() | 有界的代理人清單;每頁最多 100 | Delegation |
POST | /api/v1/auth/delegation/grants | isAuthenticated() | 指派代理人(同租戶) | Delegation |
DELETE | /api/v1/auth/delegation/grants/{id} | isAuthenticated() | 撤銷代理授權 | Delegation |
GET | /api/v1/auth/delegation/candidates | isAuthenticated() | 可指派代理人候選 | Delegation |
GET | /api/v1/auth/acting | isAuthenticated() | 當前代行脈絡(渲染橫幅/指示器) | Acting(中性共用) |
兩功能獨立、可各自 cherry-pick:Impersonation 與 Delegation 各有獨立的 controller / service,彼此零依賴(
ImpersonationService不碰任何 delegation 元件、反之亦然)——下游只想要其一時,整組拿走即可編譯運行。GET /acting(當前代行脈絡)是兩者共用的中性讀取面,由獨立的ActingController直接讀框架ActingContext,對兩功能皆中性(只用模擬 →delegating恆 false;只用代理 →impersonating恆 false)。候選名單也依功能拆成兩個端點,不再用單一?mode=混流。
閘門是政策:一般模擬使用 impersonation:enter,跨租戶另需
impersonation:cross-tenant;緊急路徑只接受 impersonation:break-glass,且不得由一般端點
自動繞過 consent。指派自己的代理人是個人行為,只驗 isAuthenticated()。
5.2 代理:授權(grant)模型
代理只有一種啟用方式——授權(grant)。 「代理」即「權限持有者把權限授給他人代為行使」,權限由授權人往下流出;不存在「受益人自行挑選要借誰的權限」(那是自我提權、不是代理)。所以代理一律是授權人指派代理人 → 代理人登入時自動生效,直到絕對期限或撤銷。grant 保存 ACTIVE/REVOKED 狀態、到期時間與撤銷資訊;同一 grantor/grantee 配對具有唯一限制,續授權會重新啟用既有資料列。適用休假代理、職務代理人等現實情境。
流程:授權人 A 在「我的代理人」頁指派代理人 B(DelegationGrant 持久化)→ B 登入時 /api/v1/auth/login 查其收到的授權、合併 A 的 ROLE 權限、以 generateDelegationToken(B, [A], …) 簽 token。B 身份不變(sub=B)、多了 A 的權限,grantors=[A]。框架只負責簽 token;授權關係的持久化與撤銷政策是應用層職責。
邊界分成三層:jar capability 提供 token/LoginActingContributor;delegation Feature
提供 DelegationApplication 窄契約;reference domain 才擁有 DelegationGrant
entity/repository、REST URL 與帳號目錄政策。登入整合由 reference DelegationService
同時實作 DelegationApplication 與其父介面 LoginActingContributor,再由框架
LoginService(ADR-023)於登入時呼叫:
// 起點:代理人自身權限。逐一併入每位授權人的 ROLE 與其下 authority
for (DelegationGrant grant : grants) {
PrincipalView grantor = principalDirectory
.findById(grant.getGrantorSubject()).orElse(null);
if (grantor == null) continue;
grantor.roleNames().forEach(role ->
mergedAuthorities.add(new SimpleGrantedAuthority(toRoleAuthority(role))));
grantor.authorityNames().forEach(authority ->
mergedAuthorities.add(new SimpleGrantedAuthority(authority)));
grantorSubjects.add(grant.getGrantorSubject());
}
AuthPrincipal mergedActor = DefaultAuthPrincipal.builder(grantee.username())
.subject(grantee.id())
.authorities(mergedAuthorities)
.build(); // sub 仍是 B 的 stable subject
// …發 DELEGATION_APPLIED AuditEvent(持久落 audit_event 表,見 §四稽核)…
app-tenantless-server 不建立 tenant context;reference grant 本身也沒有 tenant 欄位。
真正的 instance ownership 可見性由各產品把 ActingContext.getGrantors() 折入
repository/service 過濾。app-tenant-server 則在自己的模組內維護 tenant-native 政策。
LoginService 拿到合併結果(ActingLogin)後:
var acting = actingContributor.flatMap(c -> c.contribute(account.subject()));
if (acting.isPresent()) {
var login = acting.get();
accessToken = jwtTokenProvider.generateDelegationToken(
login.effectivePrincipal(), login.grantors(), claims);
}
// Refresh Session 只保存 stable subject;refresh 時重新呼叫 contributor,反映最新 grant。
清單邊界:GET /delegation/grants 預設 activeOnly=true,只回 ACTIVE 且未到期資料;
page 從 0 起、size 預設 50 且最大 100,總數與頁資訊在 X-Total-Count/X-Page/
X-Page-Size。管理歷史時才明示 activeOnly=false,避免「我的代理人」頁隨軟刪歷史無界膨脹。
關閉代理:grant 模型下代理人不能自行卸下被授予的權限(那是授權人的決定)——沒有 delegate-initiated exit 端點。要停止代理,授權人 DELETE /delegation/grants/{id} 撤銷授權(下次登入即不再套用),或代理人登出。這與模擬的 enter/exit 對稱不同:代理人從未「enter」(登入自動套用),故無對稱的「exit」。
既有 delegation_grant 升級
有既有資料的環境不得直接讓升級後 binary 以 ddl-auto=update 首次啟動:新增的
expires_at/status 是 NOT NULL,且 pair unique constraint 可能被歷史重複資料阻擋。
請先備份並在停機窗口以既有 Flyway/Liquibase/DBA 流程完成以下順序:
- 先以 nullable 方式加入
expires_at、status、revoked_at、revoked_by_subject、revoke_reason。 - 查出
grantor_subject × grantee_subject重複;依業務決定保留一筆並移除其餘,不得讓腳本任意猜 winner。 - 將舊 grant 回填為
ACTIVE,並設定明確期限。Reference 建議以 migration 時刻加 7 天,讓舊的無限期授權有受控換約窗口。 - 將
expires_at/status改為 NOT NULL,再建立uk_delegation_grant_pair。 - 以
ddl-auto=validate啟動一次確認 mapping,之後才恢復該環境既定策略。
重複資料預檢(各支援資料庫通用 SQL):
SELECT grantor_subject, grantee_subject, COUNT(*) AS duplicate_count
FROM delegation_grant
GROUP BY grantor_subject, grantee_subject
HAVING COUNT(*) > 1;
MySQL 8 參考回填:
ALTER TABLE delegation_grant
ADD COLUMN expires_at DATETIME(6) NULL,
ADD COLUMN status VARCHAR(20) NULL,
ADD COLUMN revoked_at DATETIME(6) NULL,
ADD COLUMN revoked_by_subject VARCHAR(36) NULL,
ADD COLUMN revoke_reason VARCHAR(100) NULL;
UPDATE delegation_grant
SET expires_at = DATE_ADD(UTC_TIMESTAMP(6), INTERVAL 7 DAY),
status = 'ACTIVE'
WHERE expires_at IS NULL OR status IS NULL;
-- 先人工處理上述 duplicate query 的結果,再執行 constraint。
ALTER TABLE delegation_grant
MODIFY expires_at DATETIME(6) NOT NULL,
MODIFY status VARCHAR(20) NOT NULL,
ADD CONSTRAINT uk_delegation_grant_pair UNIQUE (grantor_subject, grantee_subject);
PostgreSQL 參考回填:
ALTER TABLE delegation_grant
ADD COLUMN expires_at TIMESTAMP(6) WITH TIME ZONE,
ADD COLUMN status VARCHAR(20),
ADD COLUMN revoked_at TIMESTAMP(6) WITH TIME ZONE,
ADD COLUMN revoked_by_subject VARCHAR(36),
ADD COLUMN revoke_reason VARCHAR(100);
UPDATE delegation_grant
SET expires_at = CURRENT_TIMESTAMP + INTERVAL '7 days',
status = 'ACTIVE'
WHERE expires_at IS NULL OR status IS NULL;
-- 先人工處理上述 duplicate query 的結果,再執行 constraint。
ALTER TABLE delegation_grant
ALTER COLUMN expires_at SET NOT NULL,
ALTER COLUMN status SET NOT NULL,
ADD CONSTRAINT uk_delegation_grant_pair UNIQUE (grantor_subject, grantee_subject);
SQL Server/Oracle 使用相同五步驟,但型別與 interval 語法須由該專案 migration 工具依 dialect 產生;reference 不提供一支假裝跨方言、實際可能毀損資料的通用 DDL。
5.3 模擬:enter / exit + 跨租戶閘門
POST /impersonation 進入模擬(ImpersonationService.enter):
- 閘門
impersonation:enter由 controller@PreAuthorize把關。 - Service 補 runtime 政策:目標與自己不同租戶時須另具
impersonation:cross-tenant(一般USER因此被限縮在同租戶);拒絕模擬服務帳號、拒絕模擬自己。 - 呼叫
LoginService.switchToImpersonation(...)建立新 Refresh Session family;其subject為被模擬者、actorSubject為真實操作者,並綁定 policy reference/絕對期限/必要 actor authority;access token 的tenantId/authorities 來自被模擬者(純模擬,不繼承目標 delegation)。 - 原 actor family 以
identity-switch撤銷;refresh 由 family 的actorSubject重鑄actorclaim,避免刷新後降級成被模擬者的直接登入。 - 發
IMPERSONATION_STARTEDAuditApplicationEvent(principal=actor,data 帶subject/crossTenant/sessionId)→ 持久落audit_event表。Refresh Session 表只保存 authentication state,不取代 audit 軌。 - 每次 refresh 重驗 actor/subject 狀態、actor authority、綁定 policy 與 consent;consent 撤銷時應用亦以 policy reference 主動撤銷所有既有 family。
為何純模擬:框架層雖容許「模擬 B 且帶 B 的代理維」兩維共存(把
ActingClaims.delegation(...)merge 進extraClaims即可),但參考實作刻意讓兩功能獨立——ImpersonationService不依賴DelegationService,下游可只取模擬。需要該組合的下游自行在enterwire 即可。
POST /impersonation/exit(ImpersonationService.exit):ActingContext.current().isImpersonating() 確認確在模擬中,取 getActor() 拿回真實操作者,建立 actor 的新 family、撤銷模擬 family 並短效 blacklist 舊 access family,發 IMPERSONATION_ENDED AuditEvent,再以正常 access token 換回身份。
一般 consent 與緊急 break-glass 是兩條不同產品路徑:一般端點只接受結構政策或有效 consent;
break-glass 端點要求 IdP amr/auth_time 證明近期 MFA step-up、理由、案件編號,並簽發短效、
non-remembered、具絕對有效期的 family。首次進入會登錄 OPEN case;結案端點持久標記 CLOSED、
立即依 case number 撤銷所有 family,之後 refresh 與重新進入都 fail closed。
Reference seed 不把 impersonation:break-glass 配給預設 ADMIN/SUPER_ADMIN。普通 local login
也沒有 amr/auth_time,所以即使手動配權仍會 fail closed;只有接上企業 IdP MFA step-up 的
專用 operations 角色才應取得該 authority。
app-office/MSW 刻意不提供 break-glass UI。它只示範普通 impersonation 警示;正式產品
必須先完成 IdP step-up journey,再以 reason/caseNumber 呼叫 server API,並在緊急 session 全程
顯示更強警示。沒有 IdP 的 mock 表單不得被當成 MFA UAT 證據。
5.4 候選名單(政策在後端)
候選名單依功能拆成兩個獨立端點(不再用 ?mode= 混流),各由自己的 service 依政策過濾,前端只呈現、不自行過濾:
GET /impersonation/candidates(ImpersonationService):先要求impersonation:enter,再依 consent、保護角色/帳號與 anti-escalation 政策逐筆過濾。GET /delegation/candidates(DelegationService):同租戶、且尚未指派過(避免重複授權)。- 兩者皆排除自己與服務帳號。
5.5 前端兩級提示(app-office)
兩級各綁一個維度、由 GET /acting 回的兩正交述詞獨立觸發、彼此獨立顯示。框架容許兩維共存,故前端天生支援同時顯示(橫幅+指示器);但花店參考實作的模擬保持純粹(不併入被模擬者的代理權)、兩功能又各自獨立,故 florist 不會由模擬觸發共存——同時顯示是框架+前端就緒的能力,留給需要該組合的下游:
| 觸發述詞 | UI | 元件 | |
|---|---|---|---|
| 模擬 | impersonating(帶 actor) | 醒目橫幅「模擬中:B」+ 結束鍵 | acting-banner.tsx |
| 代理 | delegating(grantors 非空) | 低調指示器(Header chip「代理中:{授權人}」,純顯示無停止鍵;關閉由授權人撤銷 grant) | delegation-indicator.tsx |
ActingResponse 回 { impersonating, delegating, subject, actor, grantors } 兩布林 +
username 顯示欄,無單一 mode;controller 在 reference boundary 將 framework stable
subject 即時映射為 username。兩元件各自依述詞顯示,天然支援共存。進入模擬經「切換身份」
對話框(switch-identity-dialog.tsx,候選 Select);代理由授權人在「我的代理人」設定頁
(my-delegates-page.tsx)指派、代理人登入即生效。
六、導入你自己的專案
在框架四件套之上,你需要作者化下列應用層政策(可直接參考 app-tenant-server):
- 端點:模擬 enter/exit、代理 grants CRUD、acting 脈絡查詢、候選名單。
- 閘門 authority:模擬用特權 authority(+跨租戶額外 authority);代理指派依你的政策(參考實作用
isAuthenticated())。 - 持久化:代理授權關係
DelegationGrant(grantor→grantee,這是活的授權、非稽核)。 - 稽核:依需求選擇 Entity history(版本+changeset actor)及/或 request-level acting hook(
HandlerInterceptor讀ActingContext)發AuditEvent+ 持久AuditEventRepository(audit_event表);不建 acting 專屬 session 表(JWT 每請求自帶脈絡,見 §四)。 - 權限合併(代理):登入時把授權人的 ROLE 權限併入,
sub維持代理人。 - instance 級過濾(
ownership應用):讀getGrantors()折入擁有權過濾。 - 前端兩級 UI:模擬橫幅 vs 代理指示器,靠
GET /acting驅動。
框架不做的事,別在應用層繞過框架自己簽 token——一律經 JwtTokenProvider 的兩個代行方法,確保 claim 對稱、ActingContext 讀得到。
七、相關文檔
- ADR-013: 代理與模擬——ActingContext 框架能力(決策背景、方案比較、權衡)
- ADR-009: 認證授權雙模式資源伺服器(本能力疊加其上)
- ADR-001: 多租戶數據隔離策略(tenant/ownership 隔離、tenant claim 解析)
- 多租戶使用指南(
TenantContext解析鏈) - Bearer Token 統一登入端點(
/api/v1/auth/login是代理登入整合點) - 安全性模組使用指南(Spring Security 配置、token 黑名單)
外部參考
- RFC 8693(OAuth 2.0 Token Exchange,
act/may_actclaim 的既有先例,供 claim 命名參考) - Spring Security
SwitchUserFilter(switch-user 既有模式;本設計採 token-based 而非 filter-based)