题目与适用场景
一个多租户 SaaS 的 API 网关代表登录用户调用账单服务。身份平台签发的上游访问令牌面向网关,账单服务只接受自己的受众。网关需要在用户请求内取得一个更窄的下游令牌,同时保留“谁在代表谁行动”的可审计关系。
请设计令牌交换端点、验证规则、声明映射、缓存、错误处理、吊销与监控。假设网关是受控服务端,浏览器不直接保存下游令牌;账单服务必须拒绝面向其他服务的令牌。
五条不变量:
- 交换服务只接受已验证且允许被交换的 subject token。
- 新令牌的
audience、scope、租户和资源范围不得比上游授权更宽。 - 令牌中的 subject 与 actor 同时可追溯,不能把服务身份伪装成人。
- 下游服务只信任自己的 issuer、受众和签名密钥。
- 吊销、过期、密钥轮换和审计边界都有可测量的时限。
面试官在考察什么
候选人应先区分 token exchange、token forwarding、impersonation 和 delegation。RFC 8693 定义了 HTTP/JSON 的 Security Token Service;请求可以带 subjecttoken,也可以带 actortoken,响应是普通 OAuth token endpoint 响应的扩展。它不自动赋予调用方更大权限。
第二个信号是受众约束。网关不能把面向自身的 bearer token 原样转发给账单、搜索和导出服务。每个下游令牌都要写入单一 audience,并只授予本次调用所需 scope。
第三个信号是代理关系。只有 subject token 表示的主体允许该 actor 代理时才能交换;mayact、act 等声明要有明确的信任来源。把客户端提交的任意 sub 或 tenantid 复制进新令牌属于越权。
最后要讨论失败模式:交换端点缓存了旧的授权判断、下游只验签不验 audience、令牌被记录到日志、刷新令牌被错误地交给网关,以及上游吊销在下游 TTL 内仍然有效。
回答前要澄清的问题
- 谁是 subject,谁是 actor? 本题中 subject 是登录用户,actor 是 API 网关;服务账号代替用户时是否允许要写入策略。
- 交换后的令牌给谁用? 账单服务是唯一 audience,还是允许多个资源?多资源会扩大泄露爆炸半径。
- scope 如何缩小? 上游有
billing:read billing:write,本次请求只需要哪一个? - 租户边界在哪里验证? 身份声明、会话数据库和资源数据库谁是权威?不能信任请求体租户字段。
- 撤销目标是什么? 例如上游吊销后五秒内不再签发新令牌,现有下游令牌最多还有两分钟寿命。
- 网关是否需要 refresh token? 代理调用通常只需要短期 access token;把长期 refresh token 下发给网关需单独评估。
30 秒回答框架
“我会让网关调用标准 token endpoint,以 subjecttoken 表示用户、以 actortoken 表示网关,并请求固定的账单 audience 与最小 scope。交换服务先验证 issuer、签名、exp、nbf、客户端认证、subject 的可代理策略和租户状态,再通过策略表把上游 scope 映射为允许的下游 scope。新令牌只含账单 audience、短过期时间、租户和 subject/actor 关系,绝不复制未验证的声明。
账单服务只接受自己的 issuer、签名密钥和 audience,并按租户资源做二次授权。交换判断可以短缓存,但吊销通知和短 TTL 必须满足目标;所有令牌正文、交换请求和 actor secret 都脱敏。异常返回统一错误,审计记录 request ID、subject、actor、audience、scope 和策略版本。”
分步深入设计
第一步:定义令牌与信任边界
交换服务是授权服务器或受信任的 STS,不是任意网关 helper。它配置允许的上游 issuer、JWKS、客户端认证方式、下游 audience 和策略版本。网关通过 mTLS、私钥 JWT 或其他已批准的客户端认证调用端点;不能只凭一个静态字符串表明自己有权代理。
请求最少包含:
grant_type=urn:ietf:params:oauth:grant-type:token-exchange
subject_token=...
subject_token_type=urn:ietf:params:oauth:token-type:access_token
actor_token=...
actor_token_type=urn:ietf:params:oauth:token-type:access_token
audience=https://billing.internal
scope=billing:readRFC 8693 把请求方视为本次交换的 client;资源服务器也可以临时扮演 client,把收到的令牌换成适合后端服务的令牌。这个角色定义不等于授予新权限。
第二步:按来源验证 subject 与 actor
先按令牌类型选择验证器,再检查签名算法白名单、issuer、exp、nbf、必要的 client、token type 和吊销状态。JWKS 按 issuer 缓存并按 kid 轮换;未知 key 触发受控刷新,不接受算法降级。
subject 代表被代理的用户或工作负载。actor 代表实际发起交换的网关。策略要回答“这个 actor 是否能代表这个 subject 访问这个 audience”,而不是只检查 actor 是否是某个服务。RFC 8693 的 may_act 可以表达被授权的 actor,交换后令牌可用 act 表达当前 actor;二者必须来自受信任的签发方或本地策略。
不要把请求体里的 sub、tenant_id、roles 作为身份事实。交换服务从已验证令牌和权威租户目录重建上下文,并拒绝缺失或冲突的主体信息。
第三步:计算单调收窄的授权
把授权计算写成受约束的交集:
issued_scope = requested_scope
∩ subject_allowed_scope
∩ actor_allowed_scope
∩ audience_policy_scope
∩ tenant_state_scope请求 scope 为空或超出交集时应拒绝,而不是静默颁发更宽的默认 scope。audience 必须来自服务注册表,不能让网关任意传入 URL;每个 audience 绑定允许的 claim、scope、TTL 和资源类型。
租户状态、用户禁用、账单账户归属和高风险操作可以要求在线策略检查。令牌里的 tenant_id 只作为已验证上下文的一部分,下游仍需把它与资源所有权比较。若一次调用要访问多个服务,优先分别交换多个短期令牌,避免一个全能受众。
第四步:签发短期、可验证的下游令牌
新 access token 至少含 iss、sub、aud、exp、iat、scope、租户标识、client_id、策略版本和 actor 关系。下游令牌的 exp 应短于上游剩余寿命和业务风险窗口;不要把 refresh token 当作普通交换结果。
账单服务固定配置 issuer、audience、JWKS 和允许算法,并在每次请求执行资源级授权。只验签而不验 aud 会让一个服务接受发给另一个服务的令牌,正是横向滥用的入口。必要时可使用 sender-constrained token,让复制 bearer 值不足以调用下游。
第五步:控制缓存、吊销和密钥轮换
可以缓存 issuer 元数据、JWKS 和短期策略结果,但缓存键要包含 issuer、subject、actor、audience、scope、租户和策略版本。不能把一个用户的允许结果复用给另一租户。权限撤销先更新权威目录并发布失效事件;交换端点在五秒内停止签发,新令牌 TTL 设为最多两分钟,账单服务按风险选择在线 introspection 或短 TTL 验证。
缓存失效消息丢失、节点重启和复制延迟必须演练。JWKS 轮换要允许旧 key 的有界重叠,令牌 kid 可追踪;撤销旧签发密钥会立即影响所有令牌,不能拿它代替主体级吊销计划。
第六步:错误、重放与可用性
对未知 issuer、过期、错误 audience、不可代理、scope 不足和策略不可用返回统一的 invalidgrant 或 invalidtarget 类错误,不泄露主体是否存在。内部用 request ID 和安全原因码审计。
交换请求包含敏感 bearer 值,不应进入 URL、普通日志、追踪属性或错误上报。若上游令牌本身可重放,窃取网关通信的攻击者可重复交换;用 TLS、sender constraint、短 TTL、速率限制和一次性风险信号降低窗口。交换端点的策略存储不可用时,高风险写操作失败关闭;低风险读取若允许降级,也要明确最大旧数据时限。
第七步:审计主体链和跨租户保护
每次签发记录 request_id、subject、actor、client、tenant、audience、requested/issued scope、token ID、策略版本、issuer key ID 和决策结果,不记录令牌正文。下游日志引用 token ID 与 request ID,把网关调用和用户操作连成一条证据链。
资源服务不应只相信网关传来的 X-Tenant-ID。它应从签名令牌读取租户上下文,再检查资源所有权、账单账户状态和操作审批。对跨租户测试、混淆代理、scope 提升、错误 audience 和旧 token 重放建立自动化对抗用例。
第八步:用阶段性灰度验证安全不变量
先为一个测试租户注册固定 audience 和 scope,验证旧令牌不能访问账单服务。再灰度新 issuer/key,观测交换拒绝率、策略延迟、token TTL、audience 错误、缓存命中和吊销传播时间。逐步扩大到生产租户,并保留明确的回滚开关:停止签发新 token,恢复旧的受控客户端配置,而不是关闭下游所有授权检查。
高质量示范回答
“我把交换服务当作受信任的 STS。网关以认证的 client 身份提交用户 subjecttoken、自身 actortoken、固定账单 audience 和最小 scope。服务验证每个 issuer、签名、时间、类型、客户端和可代理策略,再把请求 scope 与 subject、actor、audience、租户状态策略求交集;任何扩大权限或任意 audience 的请求都拒绝。
签发的下游令牌只面向账单服务,寿命不超过上游剩余寿命,带有 subject、actor、tenant、scope、策略版本和 token ID。账单服务固定信任自己的 issuer、JWKS 和 audience,并执行资源级租户授权。令牌和交换请求不进日志,审计只保留主体链与决策元数据。
撤销先改权威目录并广播失效,五秒内停止新交换;下游令牌最多两分钟,按风险选择 introspection 或短 TTL。缓存键包含完整主体与策略维度,JWKS 轮换允许有界重叠。这样 token exchange 是权限收窄和责任传递机制,不是把用户身份复制给所有服务。”
常见错误
- 把上游 bearer token 原样转发。 下游 audience 不匹配,泄露后横向影响扩大;应为每个资源换取窄令牌。
- 只验证签名,不验证 issuer、audience 和 scope。 签名正确的令牌仍可能发给别的服务。
- 把 subject 和 actor 合并。 下游无法区分用户和代理服务,审计与撤销都失真。
- 信任请求体的
tenant_id。 攻击者可把调用改成另一租户;从已验证身份和资源归属重建。 - 把 actor 能调用交换端点当成可代理任何用户。 代理关系必须有明确策略和来源。
- 默认颁发 refresh token。 网关只需短期 access token,长期凭证会扩大泄露窗口。
- 缓存没有完整维度或 TTL 过长。 可能跨租户复用授权或延迟撤销;限定键、版本和传播目标。
- 令牌和交换请求写入日志。 bearer 值可被重放;只记录 token ID、public 元数据和原因码。
- 一个令牌覆盖所有下游。 audience 与 scope 过宽;分别交换短期令牌。
- 只测成功路径。 过期、错误 audience、吊销、JWKS 轮换、节点重启和策略故障才暴露真实边界。
追问与参考答案
may_act 和 act 分别解决什么问题?
may_act 表达 subject token 允许哪个 actor 代理;act 表达新令牌当前由谁代表谁行动。两者都不能替代签名、issuer、audience 和本地授权检查,且声明来源必须可信。
为什么不直接把用户令牌转发给账单服务?
上游令牌通常面向网关,scope 可能覆盖多个资源。转发会扩大受众、暴露更多身份和权限,并让下游承担不必要的信任。交换能签发只面向账单、短 TTL、可审计的令牌。
下游服务要不要实时 introspection?
取决于吊销目标、流量和可用性。高风险写操作可在线 introspection;普通读取可使用短 TTL JWT。必须记录最坏延迟,不能同时宣称离线高缓存和即时吊销。
如果策略服务暂时不可用怎么办?
交换端点对高风险操作失败关闭,避免在未知状态下签发。低风险读取若有明确旧策略缓存,可在最长时限内降级,并把降级计入监控和审计。默认放行会把可用性故障变成越权。
多租户服务如何防止缓存串租户?
缓存键至少包含 issuer、subject、actor、tenant、audience、scope 和策略版本;资源服务还要做资源所有权检查。仅按用户 ID 或 audience 缓存会把一个租户的决策泄露给另一个租户。
上游令牌已吊销但下游令牌仍未过期,怎么办?
主体吊销事件应使交换立即停止,并按风险让下游 introspection 或消费失效事件。若下游只能离线验签,就把 TTL 限制在可接受的最大窗口;无法撤回已经提交的操作,需审计该时间段。
如何做安全回滚?
停止新 exchange、撤回有问题的 audience 或策略版本,保留旧签发 key 的有界验证能力,并恢复已验证的客户端配置。不要删除审计记录或关闭下游 audience 检查来“快速恢复”。