多租户身份代理架构实战:从联合登录到会话生命周期的完整设计

当一个 SaaS 产品同时服务数百家企业客户,而每家客户都带着自己的身份系统(Azure AD、Okta、自建 OIDC、LDAP)要求员工用公司账号登录时,身份认证就从「我有一个登录系统」演变成一道架构难题:如何让一套系统,安全地同时对接 N 个彼此独立、协议各异的身份提供方,同时让自己的业务应用毫无感知?

这道题的标准答案是身份代理(Identity Broker)。本文系统梳理这一架构:它的角色定位、Discovery 端点的双层结构、租户识别(家域发现)的策略选型、身份映射的落地细节,以及最容易被忽视的运行期会话生命周期------刷新与登出究竟还要不要惊动真实 IdP。文章融合了从基础概念到工程红线的完整链路。


一、核心架构:为什么需要身份代理

1.1 角色叠加的矛盾

OIDC(OpenID Connect)里有两个核心角色:**OP(OpenID Provider,身份提供方)**签发身份;RP(Relying Party,依赖方)消费身份。多租户联合登录的复杂性,源于你的 SaaS 必须同时扮演两个角色:

  • 对你的业务应用 而言,你是 OP------应用只信任你、只对接你一个 issuer;
  • 对各租户的真实 IdP 而言,你又是 RP------你要去消费字节 Azure AD、银行 OIDC 签发的身份。

1.2 身份代理:中间做身份转换

解法是让 SaaS 居中转换:对上游 IdP 当 RP 收身份,对下游应用当 OP 发身份。
#mermaid-svg-5cZErFRDux2hFUaR{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-5cZErFRDux2hFUaR .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-5cZErFRDux2hFUaR .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-5cZErFRDux2hFUaR .error-icon{fill:#552222;}#mermaid-svg-5cZErFRDux2hFUaR .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-5cZErFRDux2hFUaR .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-5cZErFRDux2hFUaR .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-5cZErFRDux2hFUaR .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-5cZErFRDux2hFUaR .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-5cZErFRDux2hFUaR .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-5cZErFRDux2hFUaR .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-5cZErFRDux2hFUaR .marker{fill:#333333;stroke:#333333;}#mermaid-svg-5cZErFRDux2hFUaR .marker.cross{stroke:#333333;}#mermaid-svg-5cZErFRDux2hFUaR svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-5cZErFRDux2hFUaR p{margin:0;}#mermaid-svg-5cZErFRDux2hFUaR .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-5cZErFRDux2hFUaR .cluster-label text{fill:#333;}#mermaid-svg-5cZErFRDux2hFUaR .cluster-label span{color:#333;}#mermaid-svg-5cZErFRDux2hFUaR .cluster-label span p{background-color:transparent;}#mermaid-svg-5cZErFRDux2hFUaR .label text,#mermaid-svg-5cZErFRDux2hFUaR span{fill:#333;color:#333;}#mermaid-svg-5cZErFRDux2hFUaR .node rect,#mermaid-svg-5cZErFRDux2hFUaR .node circle,#mermaid-svg-5cZErFRDux2hFUaR .node ellipse,#mermaid-svg-5cZErFRDux2hFUaR .node polygon,#mermaid-svg-5cZErFRDux2hFUaR .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-5cZErFRDux2hFUaR .rough-node .label text,#mermaid-svg-5cZErFRDux2hFUaR .node .label text,#mermaid-svg-5cZErFRDux2hFUaR .image-shape .label,#mermaid-svg-5cZErFRDux2hFUaR .icon-shape .label{text-anchor:middle;}#mermaid-svg-5cZErFRDux2hFUaR .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-5cZErFRDux2hFUaR .rough-node .label,#mermaid-svg-5cZErFRDux2hFUaR .node .label,#mermaid-svg-5cZErFRDux2hFUaR .image-shape .label,#mermaid-svg-5cZErFRDux2hFUaR .icon-shape .label{text-align:center;}#mermaid-svg-5cZErFRDux2hFUaR .node.clickable{cursor:pointer;}#mermaid-svg-5cZErFRDux2hFUaR .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-5cZErFRDux2hFUaR .arrowheadPath{fill:#333333;}#mermaid-svg-5cZErFRDux2hFUaR .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-5cZErFRDux2hFUaR .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-5cZErFRDux2hFUaR .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-5cZErFRDux2hFUaR .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-5cZErFRDux2hFUaR .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-5cZErFRDux2hFUaR .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-5cZErFRDux2hFUaR .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-5cZErFRDux2hFUaR .cluster text{fill:#333;}#mermaid-svg-5cZErFRDux2hFUaR .cluster span{color:#333;}#mermaid-svg-5cZErFRDux2hFUaR div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-5cZErFRDux2hFUaR .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-5cZErFRDux2hFUaR rect.text{fill:none;stroke-width:0;}#mermaid-svg-5cZErFRDux2hFUaR .icon-shape,#mermaid-svg-5cZErFRDux2hFUaR .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-5cZErFRDux2hFUaR .icon-shape p,#mermaid-svg-5cZErFRDux2hFUaR .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-5cZErFRDux2hFUaR .icon-shape .label rect,#mermaid-svg-5cZErFRDux2hFUaR .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-5cZErFRDux2hFUaR .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-5cZErFRDux2hFUaR .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-5cZErFRDux2hFUaR :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 下游 业务应用
你的SaaS 身份代理
上游 各租户真实IdP
字节 Azure AD
银行 自建OIDC
小客户 LDAP/SAML
RP侧

消费上游IdP
身份映射/联合

外部身份→内部用户
OP侧

签发自己的令牌
应用A
应用B

下游应用永远只看到你这一个 OP、一个 issuer,完全不感知背后有多少真实 IdP。代理把异构的上游身份,统一成同构的下游身份。 Keycloak 的 "Identity Brokering"、Auth0 的 "Enterprise Connections" 都是这一模式的成熟实现。

1.3 代理模式的核心价值

价值 说明
解耦 应用只对接一个 OP;新增/变更租户 IdP 时应用零改动,全部收敛在代理层
统一身份模型 无论上游是 OIDC、SAML 还是 LDAP,代理对下游统一输出 OIDC 令牌(可做协议转换)
集中安全策略 MFA、风控、会话管理、审计统一在代理实现,无需每个应用各做一遍
租户隔离单一执行点 iss 校验、租户绑定、唯一键约束都在代理一处把关,边界清晰

二、双层 Discovery:谁生产,谁消费

身份代理模式下,Discovery 端点(/.well-known/openid-configuration)存在两层,方向相反,极易混淆。
#mermaid-svg-iWtSl93jKiWrlKXw{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-iWtSl93jKiWrlKXw .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-iWtSl93jKiWrlKXw .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-iWtSl93jKiWrlKXw .error-icon{fill:#552222;}#mermaid-svg-iWtSl93jKiWrlKXw .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-iWtSl93jKiWrlKXw .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-iWtSl93jKiWrlKXw .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-iWtSl93jKiWrlKXw .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-iWtSl93jKiWrlKXw .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-iWtSl93jKiWrlKXw .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-iWtSl93jKiWrlKXw .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-iWtSl93jKiWrlKXw .marker{fill:#333333;stroke:#333333;}#mermaid-svg-iWtSl93jKiWrlKXw .marker.cross{stroke:#333333;}#mermaid-svg-iWtSl93jKiWrlKXw svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-iWtSl93jKiWrlKXw p{margin:0;}#mermaid-svg-iWtSl93jKiWrlKXw .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-iWtSl93jKiWrlKXw .cluster-label text{fill:#333;}#mermaid-svg-iWtSl93jKiWrlKXw .cluster-label span{color:#333;}#mermaid-svg-iWtSl93jKiWrlKXw .cluster-label span p{background-color:transparent;}#mermaid-svg-iWtSl93jKiWrlKXw .label text,#mermaid-svg-iWtSl93jKiWrlKXw span{fill:#333;color:#333;}#mermaid-svg-iWtSl93jKiWrlKXw .node rect,#mermaid-svg-iWtSl93jKiWrlKXw .node circle,#mermaid-svg-iWtSl93jKiWrlKXw .node ellipse,#mermaid-svg-iWtSl93jKiWrlKXw .node polygon,#mermaid-svg-iWtSl93jKiWrlKXw .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-iWtSl93jKiWrlKXw .rough-node .label text,#mermaid-svg-iWtSl93jKiWrlKXw .node .label text,#mermaid-svg-iWtSl93jKiWrlKXw .image-shape .label,#mermaid-svg-iWtSl93jKiWrlKXw .icon-shape .label{text-anchor:middle;}#mermaid-svg-iWtSl93jKiWrlKXw .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-iWtSl93jKiWrlKXw .rough-node .label,#mermaid-svg-iWtSl93jKiWrlKXw .node .label,#mermaid-svg-iWtSl93jKiWrlKXw .image-shape .label,#mermaid-svg-iWtSl93jKiWrlKXw .icon-shape .label{text-align:center;}#mermaid-svg-iWtSl93jKiWrlKXw .node.clickable{cursor:pointer;}#mermaid-svg-iWtSl93jKiWrlKXw .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-iWtSl93jKiWrlKXw .arrowheadPath{fill:#333333;}#mermaid-svg-iWtSl93jKiWrlKXw .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-iWtSl93jKiWrlKXw .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-iWtSl93jKiWrlKXw .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-iWtSl93jKiWrlKXw .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-iWtSl93jKiWrlKXw .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-iWtSl93jKiWrlKXw .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-iWtSl93jKiWrlKXw .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-iWtSl93jKiWrlKXw .cluster text{fill:#333;}#mermaid-svg-iWtSl93jKiWrlKXw .cluster span{color:#333;}#mermaid-svg-iWtSl93jKiWrlKXw div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-iWtSl93jKiWrlKXw .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-iWtSl93jKiWrlKXw rect.text{fill:none;stroke-width:0;}#mermaid-svg-iWtSl93jKiWrlKXw .icon-shape,#mermaid-svg-iWtSl93jKiWrlKXw .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-iWtSl93jKiWrlKXw .icon-shape p,#mermaid-svg-iWtSl93jKiWrlKXw .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-iWtSl93jKiWrlKXw .icon-shape .label rect,#mermaid-svg-iWtSl93jKiWrlKXw .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-iWtSl93jKiWrlKXw .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-iWtSl93jKiWrlKXw .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-iWtSl93jKiWrlKXw :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 下游Discovery
你的SaaS(OP侧)发布
业务应用 消费
上游Discovery
真实IdP 发布
你的SaaS(RP侧)消费

谁发布 谁消费 issuer 归属
上游 各租户真实 IdP 你的 SaaS(RP侧) 真实 IdP 的固定 issuer
下游 你的 SaaS(OP侧) 业务应用 你的 SaaS 按租户的 issuer

2.1 上游:按租户消费各 IdP 的 Discovery

每个租户在配置库里存着对应 IdP 的固定 issuer。请求到来时识别租户,取出 issuer,拼地址拉取(结果可缓存):

复制代码
租户A → https://login.microsoftonline.com/aaaa/v2.0/.well-known/openid-configuration
租户B → https://accounts.google.com/.well-known/openid-configuration

2.2 下游:发布带租户隔离的 Discovery

既然对应用你是 OP,就要生产 Discovery。推荐「每租户独立 issuer,路径区分」:

复制代码
https://auth.yoursaas.com/tenant-a/.well-known/openid-configuration
   → issuer: https://auth.yoursaas.com/tenant-a

服务端按 URL 中的租户段动态生成元数据。这正是 Azure AD 的做法(login.microsoftonline.com/{tenantId}/v2.0/...)。它不是转发,而是按租户上下文动态生成并返回。

2.3 issuer 校验:固定性与一致性自检

关键澄清:issuer 始终是固定 的;「等于请求基地址」不是说它会变,而是一条防伪自检 ------你拉 Discovery 用的地址本就是基于已知固定 issuer 拼的,拉回文档里的 issuer 必须等于这个预期固定值,后续 token 的 iss 也必须等于它。
#mermaid-svg-PZ7tLKS07xbu3BCN{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-PZ7tLKS07xbu3BCN .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-PZ7tLKS07xbu3BCN .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-PZ7tLKS07xbu3BCN .error-icon{fill:#552222;}#mermaid-svg-PZ7tLKS07xbu3BCN .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-PZ7tLKS07xbu3BCN .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-PZ7tLKS07xbu3BCN .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-PZ7tLKS07xbu3BCN .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-PZ7tLKS07xbu3BCN .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-PZ7tLKS07xbu3BCN .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-PZ7tLKS07xbu3BCN .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-PZ7tLKS07xbu3BCN .marker{fill:#333333;stroke:#333333;}#mermaid-svg-PZ7tLKS07xbu3BCN .marker.cross{stroke:#333333;}#mermaid-svg-PZ7tLKS07xbu3BCN svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-PZ7tLKS07xbu3BCN p{margin:0;}#mermaid-svg-PZ7tLKS07xbu3BCN .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-PZ7tLKS07xbu3BCN .cluster-label text{fill:#333;}#mermaid-svg-PZ7tLKS07xbu3BCN .cluster-label span{color:#333;}#mermaid-svg-PZ7tLKS07xbu3BCN .cluster-label span p{background-color:transparent;}#mermaid-svg-PZ7tLKS07xbu3BCN .label text,#mermaid-svg-PZ7tLKS07xbu3BCN span{fill:#333;color:#333;}#mermaid-svg-PZ7tLKS07xbu3BCN .node rect,#mermaid-svg-PZ7tLKS07xbu3BCN .node circle,#mermaid-svg-PZ7tLKS07xbu3BCN .node ellipse,#mermaid-svg-PZ7tLKS07xbu3BCN .node polygon,#mermaid-svg-PZ7tLKS07xbu3BCN .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-PZ7tLKS07xbu3BCN .rough-node .label text,#mermaid-svg-PZ7tLKS07xbu3BCN .node .label text,#mermaid-svg-PZ7tLKS07xbu3BCN .image-shape .label,#mermaid-svg-PZ7tLKS07xbu3BCN .icon-shape .label{text-anchor:middle;}#mermaid-svg-PZ7tLKS07xbu3BCN .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-PZ7tLKS07xbu3BCN .rough-node .label,#mermaid-svg-PZ7tLKS07xbu3BCN .node .label,#mermaid-svg-PZ7tLKS07xbu3BCN .image-shape .label,#mermaid-svg-PZ7tLKS07xbu3BCN .icon-shape .label{text-align:center;}#mermaid-svg-PZ7tLKS07xbu3BCN .node.clickable{cursor:pointer;}#mermaid-svg-PZ7tLKS07xbu3BCN .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-PZ7tLKS07xbu3BCN .arrowheadPath{fill:#333333;}#mermaid-svg-PZ7tLKS07xbu3BCN .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-PZ7tLKS07xbu3BCN .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-PZ7tLKS07xbu3BCN .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-PZ7tLKS07xbu3BCN .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-PZ7tLKS07xbu3BCN .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-PZ7tLKS07xbu3BCN .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-PZ7tLKS07xbu3BCN .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-PZ7tLKS07xbu3BCN .cluster text{fill:#333;}#mermaid-svg-PZ7tLKS07xbu3BCN .cluster span{color:#333;}#mermaid-svg-PZ7tLKS07xbu3BCN div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-PZ7tLKS07xbu3BCN .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-PZ7tLKS07xbu3BCN rect.text{fill:none;stroke-width:0;}#mermaid-svg-PZ7tLKS07xbu3BCN .icon-shape,#mermaid-svg-PZ7tLKS07xbu3BCN .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-PZ7tLKS07xbu3BCN .icon-shape p,#mermaid-svg-PZ7tLKS07xbu3BCN .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-PZ7tLKS07xbu3BCN .icon-shape .label rect,#mermaid-svg-PZ7tLKS07xbu3BCN .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-PZ7tLKS07xbu3BCN .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-PZ7tLKS07xbu3BCN .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-PZ7tLKS07xbu3BCN :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 必须相等
必须相等
不等
不等
配置里固定信任的issuer
Discovery返回的issuer
ID Token的iss
拒绝, 可能被偷换

校验的本质:确认我实际拿到的,就是我预期信任的那个固定值,防止中途被偷换。 这是后文防御 IdP 混淆攻击的根基。


三、家域发现:认证之前先判断租户

3.1 问题本质

用户来登录时系统还没认证他 ------既然还不知道他是谁,又怎么知道路由到哪个 IdP?这就是家域发现(Home Realm Discovery):在认证之前,先判断用户属于哪个租户。这是多租户与单一 OP 最本质的区别------路由决策必须前置。

3.2 四种策略的场景与权衡

#mermaid-svg-caO20NNQGrsJEggl{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-caO20NNQGrsJEggl .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-caO20NNQGrsJEggl .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-caO20NNQGrsJEggl .error-icon{fill:#552222;}#mermaid-svg-caO20NNQGrsJEggl .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-caO20NNQGrsJEggl .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-caO20NNQGrsJEggl .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-caO20NNQGrsJEggl .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-caO20NNQGrsJEggl .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-caO20NNQGrsJEggl .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-caO20NNQGrsJEggl .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-caO20NNQGrsJEggl .marker{fill:#333333;stroke:#333333;}#mermaid-svg-caO20NNQGrsJEggl .marker.cross{stroke:#333333;}#mermaid-svg-caO20NNQGrsJEggl svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-caO20NNQGrsJEggl p{margin:0;}#mermaid-svg-caO20NNQGrsJEggl .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-caO20NNQGrsJEggl .cluster-label text{fill:#333;}#mermaid-svg-caO20NNQGrsJEggl .cluster-label span{color:#333;}#mermaid-svg-caO20NNQGrsJEggl .cluster-label span p{background-color:transparent;}#mermaid-svg-caO20NNQGrsJEggl .label text,#mermaid-svg-caO20NNQGrsJEggl span{fill:#333;color:#333;}#mermaid-svg-caO20NNQGrsJEggl .node rect,#mermaid-svg-caO20NNQGrsJEggl .node circle,#mermaid-svg-caO20NNQGrsJEggl .node ellipse,#mermaid-svg-caO20NNQGrsJEggl .node polygon,#mermaid-svg-caO20NNQGrsJEggl .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-caO20NNQGrsJEggl .rough-node .label text,#mermaid-svg-caO20NNQGrsJEggl .node .label text,#mermaid-svg-caO20NNQGrsJEggl .image-shape .label,#mermaid-svg-caO20NNQGrsJEggl .icon-shape .label{text-anchor:middle;}#mermaid-svg-caO20NNQGrsJEggl .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-caO20NNQGrsJEggl .rough-node .label,#mermaid-svg-caO20NNQGrsJEggl .node .label,#mermaid-svg-caO20NNQGrsJEggl .image-shape .label,#mermaid-svg-caO20NNQGrsJEggl .icon-shape .label{text-align:center;}#mermaid-svg-caO20NNQGrsJEggl .node.clickable{cursor:pointer;}#mermaid-svg-caO20NNQGrsJEggl .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-caO20NNQGrsJEggl .arrowheadPath{fill:#333333;}#mermaid-svg-caO20NNQGrsJEggl .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-caO20NNQGrsJEggl .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-caO20NNQGrsJEggl .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-caO20NNQGrsJEggl .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-caO20NNQGrsJEggl .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-caO20NNQGrsJEggl .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-caO20NNQGrsJEggl .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-caO20NNQGrsJEggl .cluster text{fill:#333;}#mermaid-svg-caO20NNQGrsJEggl .cluster span{color:#333;}#mermaid-svg-caO20NNQGrsJEggl div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-caO20NNQGrsJEggl .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-caO20NNQGrsJEggl rect.text{fill:none;stroke-width:0;}#mermaid-svg-caO20NNQGrsJEggl .icon-shape,#mermaid-svg-caO20NNQGrsJEggl .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-caO20NNQGrsJEggl .icon-shape p,#mermaid-svg-caO20NNQGrsJEggl .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-caO20NNQGrsJEggl .icon-shape .label rect,#mermaid-svg-caO20NNQGrsJEggl .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-caO20NNQGrsJEggl .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-caO20NNQGrsJEggl .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-caO20NNQGrsJEggl :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 邮箱域名
专属子域名
URL路径
显式选择
用户访问登录
识别策略
alice@bytedance.com
bytedance.yoursaas.com
yoursaas.com/t/bytedance
页面点选企业

策略 核心优点 核心缺点 最适场景
邮箱域名映射 用户零心智负担、域名天然唯一、支持渐进式认证 多一步输邮箱;公共邮箱(gmail)无法映射;一企多域名需全配 通用 B2B SaaS 默认方案
专属子域名 上下文最强、可品牌化、无需先输邮箱、Cookie 天然隔离 需通配证书+动态 DNS;用户要记地址;自定义域名更复杂 中大型客户、强品牌诉求
URL 路径 实现最简、单证书、便于调试 URL 不专业;易被遍历试探;Cookie 隔离弱 MVP、内部工具
显式选择 无歧义、实现直观、适合多租户身份 租户多时体验崩;暴露客户名单 租户数量可控、面向已知用户

3.3 实战组合

成熟产品很少只用一种:
#mermaid-svg-EtTfLkYYoOCbu6YY{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-EtTfLkYYoOCbu6YY .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-EtTfLkYYoOCbu6YY .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-EtTfLkYYoOCbu6YY .error-icon{fill:#552222;}#mermaid-svg-EtTfLkYYoOCbu6YY .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-EtTfLkYYoOCbu6YY .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-EtTfLkYYoOCbu6YY .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-EtTfLkYYoOCbu6YY .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-EtTfLkYYoOCbu6YY .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-EtTfLkYYoOCbu6YY .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-EtTfLkYYoOCbu6YY .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-EtTfLkYYoOCbu6YY .marker{fill:#333333;stroke:#333333;}#mermaid-svg-EtTfLkYYoOCbu6YY .marker.cross{stroke:#333333;}#mermaid-svg-EtTfLkYYoOCbu6YY svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-EtTfLkYYoOCbu6YY p{margin:0;}#mermaid-svg-EtTfLkYYoOCbu6YY .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-EtTfLkYYoOCbu6YY .cluster-label text{fill:#333;}#mermaid-svg-EtTfLkYYoOCbu6YY .cluster-label span{color:#333;}#mermaid-svg-EtTfLkYYoOCbu6YY .cluster-label span p{background-color:transparent;}#mermaid-svg-EtTfLkYYoOCbu6YY .label text,#mermaid-svg-EtTfLkYYoOCbu6YY span{fill:#333;color:#333;}#mermaid-svg-EtTfLkYYoOCbu6YY .node rect,#mermaid-svg-EtTfLkYYoOCbu6YY .node circle,#mermaid-svg-EtTfLkYYoOCbu6YY .node ellipse,#mermaid-svg-EtTfLkYYoOCbu6YY .node polygon,#mermaid-svg-EtTfLkYYoOCbu6YY .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-EtTfLkYYoOCbu6YY .rough-node .label text,#mermaid-svg-EtTfLkYYoOCbu6YY .node .label text,#mermaid-svg-EtTfLkYYoOCbu6YY .image-shape .label,#mermaid-svg-EtTfLkYYoOCbu6YY .icon-shape .label{text-anchor:middle;}#mermaid-svg-EtTfLkYYoOCbu6YY .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-EtTfLkYYoOCbu6YY .rough-node .label,#mermaid-svg-EtTfLkYYoOCbu6YY .node .label,#mermaid-svg-EtTfLkYYoOCbu6YY .image-shape .label,#mermaid-svg-EtTfLkYYoOCbu6YY .icon-shape .label{text-align:center;}#mermaid-svg-EtTfLkYYoOCbu6YY .node.clickable{cursor:pointer;}#mermaid-svg-EtTfLkYYoOCbu6YY .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-EtTfLkYYoOCbu6YY .arrowheadPath{fill:#333333;}#mermaid-svg-EtTfLkYYoOCbu6YY .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-EtTfLkYYoOCbu6YY .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-EtTfLkYYoOCbu6YY .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-EtTfLkYYoOCbu6YY .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-EtTfLkYYoOCbu6YY .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-EtTfLkYYoOCbu6YY .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-EtTfLkYYoOCbu6YY .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-EtTfLkYYoOCbu6YY .cluster text{fill:#333;}#mermaid-svg-EtTfLkYYoOCbu6YY .cluster span{color:#333;}#mermaid-svg-EtTfLkYYoOCbu6YY div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-EtTfLkYYoOCbu6YY .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-EtTfLkYYoOCbu6YY rect.text{fill:none;stroke-width:0;}#mermaid-svg-EtTfLkYYoOCbu6YY .icon-shape,#mermaid-svg-EtTfLkYYoOCbu6YY .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-EtTfLkYYoOCbu6YY .icon-shape p,#mermaid-svg-EtTfLkYYoOCbu6YY .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-EtTfLkYYoOCbu6YY .icon-shape .label rect,#mermaid-svg-EtTfLkYYoOCbu6YY .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-EtTfLkYYoOCbu6YY .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-EtTfLkYYoOCbu6YY .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-EtTfLkYYoOCbu6YY :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 是


公共/未配置域名
进入登录
从专属子域名进入?
租户已定, 直跳IdP
输入企业邮箱
域名可映射?
路由到对应IdP
降级: 密码登录 或 显式选择

子域名作大客户品牌化入口,邮箱域名作通用主路径,显式选择/密码登录作兜底。


四、双跳认证:完整登录流程

身份代理的登录是「两段 OIDC 流程的串联」------用户先到代理,代理再到真实 IdP,回来后代理签发自己的令牌给应用。
字节真实IdP 代理(同时OP和RP) 业务应用 字节员工 字节真实IdP 代理(同时OP和RP) 业务应用 字节员工 #mermaid-svg-W3UgxgJ7DEfMqaez{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-W3UgxgJ7DEfMqaez .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-W3UgxgJ7DEfMqaez .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-W3UgxgJ7DEfMqaez .error-icon{fill:#552222;}#mermaid-svg-W3UgxgJ7DEfMqaez .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-W3UgxgJ7DEfMqaez .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-W3UgxgJ7DEfMqaez .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-W3UgxgJ7DEfMqaez .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-W3UgxgJ7DEfMqaez .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-W3UgxgJ7DEfMqaez .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-W3UgxgJ7DEfMqaez .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-W3UgxgJ7DEfMqaez .marker{fill:#333333;stroke:#333333;}#mermaid-svg-W3UgxgJ7DEfMqaez .marker.cross{stroke:#333333;}#mermaid-svg-W3UgxgJ7DEfMqaez svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-W3UgxgJ7DEfMqaez p{margin:0;}#mermaid-svg-W3UgxgJ7DEfMqaez .actor{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-W3UgxgJ7DEfMqaez text.actor>tspan{fill:black;stroke:none;}#mermaid-svg-W3UgxgJ7DEfMqaez .actor-line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-W3UgxgJ7DEfMqaez .innerArc{stroke-width:1.5;stroke-dasharray:none;}#mermaid-svg-W3UgxgJ7DEfMqaez .messageLine0{stroke-width:1.5;stroke-dasharray:none;stroke:#333;}#mermaid-svg-W3UgxgJ7DEfMqaez .messageLine1{stroke-width:1.5;stroke-dasharray:2,2;stroke:#333;}#mermaid-svg-W3UgxgJ7DEfMqaez #arrowhead path{fill:#333;stroke:#333;}#mermaid-svg-W3UgxgJ7DEfMqaez .sequenceNumber{fill:white;}#mermaid-svg-W3UgxgJ7DEfMqaez #sequencenumber{fill:#333;}#mermaid-svg-W3UgxgJ7DEfMqaez #crosshead path{fill:#333;stroke:#333;}#mermaid-svg-W3UgxgJ7DEfMqaez .messageText{fill:#333;stroke:none;}#mermaid-svg-W3UgxgJ7DEfMqaez .labelBox{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-W3UgxgJ7DEfMqaez .labelText,#mermaid-svg-W3UgxgJ7DEfMqaez .labelText>tspan{fill:black;stroke:none;}#mermaid-svg-W3UgxgJ7DEfMqaez .loopText,#mermaid-svg-W3UgxgJ7DEfMqaez .loopText>tspan{fill:black;stroke:none;}#mermaid-svg-W3UgxgJ7DEfMqaez .loopLine{stroke-width:2px;stroke-dasharray:2,2;stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-W3UgxgJ7DEfMqaez .note{stroke:#aaaa33;fill:#fff5ad;}#mermaid-svg-W3UgxgJ7DEfMqaez .noteText,#mermaid-svg-W3UgxgJ7DEfMqaez .noteText>tspan{fill:black;stroke:none;}#mermaid-svg-W3UgxgJ7DEfMqaez .activation0{fill:#f4f4f4;stroke:#666;}#mermaid-svg-W3UgxgJ7DEfMqaez .activation1{fill:#f4f4f4;stroke:#666;}#mermaid-svg-W3UgxgJ7DEfMqaez .activation2{fill:#f4f4f4;stroke:#666;}#mermaid-svg-W3UgxgJ7DEfMqaez .actorPopupMenu{position:absolute;}#mermaid-svg-W3UgxgJ7DEfMqaez .actorPopupMenuPanel{position:absolute;fill:#ECECFF;box-shadow:0px 8px 16px 0px rgba(0,0,0,0.2);filter:drop-shadow(3px 5px 2px rgb(0 0 0 / 0.4));}#mermaid-svg-W3UgxgJ7DEfMqaez .actor-man line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-W3UgxgJ7DEfMqaez .actor-man circle,#mermaid-svg-W3UgxgJ7DEfMqaez line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;stroke-width:2px;}#mermaid-svg-W3UgxgJ7DEfMqaez :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 第一跳 应用→代理 (你是OP) 第二跳 代理→真实IdP (你是RP) 用字节公钥验签校验 iss==字节固定issuer 回到第一跳 代理签发自己的令牌 用你的公钥验签校验 iss==你的tenant issuer 访问应用重定向到你的authorization_endpoint (tenant=bytedance)家域发现, 查出字节上游IdP配置重定向到字节authorization_endpoint用公司账号认证回调code换取IdP的ID Token身份映射: 外部身份→内部用户, 绑定租户回调code (你签发的)换取*你的*ID Token登录成功

关键:应用拿到的是「你代理签发的令牌」,不是真实 IdP 的令牌。 代理用真实 IdP 公钥验完上游令牌后,用自己的私钥重新签一个下游令牌。两段流程各自独立做 issuer 校验。


五、身份映射:代理的灵魂

映射要解决四个递进问题:用谁做唯一键 → 首次怎么建号 → 属性权限怎么转 → 多重身份怎么合并。

5.1 唯一键:必须 (iss, sub) 联合

这是映射的地基。OIDC 规定 sub 仅在单个 issuer 内唯一 ------字节 IdP 的 sub=12345 和银行 IdP 的 sub=12345 可能是两个人。只用 sub 会导致跨租户身份碰撞,是灾难性的越权漏洞。

复制代码
唯一键 = iss + "|" + sub
  https://login.microsoftonline.com/bytedance | a1b2   ← 不同iss
  https://bank-idp.example.com                | a1b2   ← sub同但是两个人

不要用 email 做主键:email 会变、可能被回收、各 IdP 格式不一。email 只适合做展示和账号链接的辅助线索。

5.2 内部数据模型:用户与外部身份分离

#mermaid-svg-0JIcQOIoseycYySI{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-0JIcQOIoseycYySI .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-0JIcQOIoseycYySI .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-0JIcQOIoseycYySI .error-icon{fill:#552222;}#mermaid-svg-0JIcQOIoseycYySI .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-0JIcQOIoseycYySI .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-0JIcQOIoseycYySI .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-0JIcQOIoseycYySI .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-0JIcQOIoseycYySI .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-0JIcQOIoseycYySI .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-0JIcQOIoseycYySI .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-0JIcQOIoseycYySI .marker{fill:#333333;stroke:#333333;}#mermaid-svg-0JIcQOIoseycYySI .marker.cross{stroke:#333333;}#mermaid-svg-0JIcQOIoseycYySI svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-0JIcQOIoseycYySI p{margin:0;}#mermaid-svg-0JIcQOIoseycYySI .entityBox{fill:#ECECFF;stroke:#9370DB;}#mermaid-svg-0JIcQOIoseycYySI .relationshipLabelBox{fill:hsl(80, 100%, 96.2745098039%);opacity:0.7;background-color:hsl(80, 100%, 96.2745098039%);}#mermaid-svg-0JIcQOIoseycYySI .relationshipLabelBox rect{opacity:0.5;}#mermaid-svg-0JIcQOIoseycYySI .labelBkg{background-color:rgba(248.6666666666, 255, 235.9999999999, 0.5);}#mermaid-svg-0JIcQOIoseycYySI .edgeLabel .label{fill:#9370DB;font-size:14px;}#mermaid-svg-0JIcQOIoseycYySI .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-0JIcQOIoseycYySI .edge-pattern-dashed{stroke-dasharray:8,8;}#mermaid-svg-0JIcQOIoseycYySI .node rect,#mermaid-svg-0JIcQOIoseycYySI .node circle,#mermaid-svg-0JIcQOIoseycYySI .node ellipse,#mermaid-svg-0JIcQOIoseycYySI .node polygon{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-0JIcQOIoseycYySI .relationshipLine{stroke:#333333;stroke-width:1;fill:none;}#mermaid-svg-0JIcQOIoseycYySI .marker{fill:none!important;stroke:#333333!important;stroke-width:1;}#mermaid-svg-0JIcQOIoseycYySI .edgeLabel{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-0JIcQOIoseycYySI .edgeLabel .label rect{fill:rgba(232,232,232, 0.8);}#mermaid-svg-0JIcQOIoseycYySI .edgeLabel .label text{fill:#333;}#mermaid-svg-0JIcQOIoseycYySI :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 拥有
INTERNAL_USER
string
user_id
PK
内部稳定ID
string
tenant_id
所属租户
string
display_name
string
primary_email
string
status
active/disabled
EXTERNAL_IDENTITY
string
ext_id
PK
string
user_id
FK
string
iss
上游issuer
string
sub
上游subject
string
tenant_id
来源租户
datetime
last_login

内部 user_id 自己生成、永久稳定,与外部 sub 解耦------即使租户换 IdP(sub 全变)也能通过账号链接保住内部用户。(iss, sub) 加数据库唯一约束,从底层杜绝碰撞。

5.3 首次登录:JIT 建号 与 SCIM 预置

#mermaid-svg-gbW16QDj0J6qQyWZ{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-gbW16QDj0J6qQyWZ .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-gbW16QDj0J6qQyWZ .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-gbW16QDj0J6qQyWZ .error-icon{fill:#552222;}#mermaid-svg-gbW16QDj0J6qQyWZ .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-gbW16QDj0J6qQyWZ .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-gbW16QDj0J6qQyWZ .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-gbW16QDj0J6qQyWZ .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-gbW16QDj0J6qQyWZ .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-gbW16QDj0J6qQyWZ .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-gbW16QDj0J6qQyWZ .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-gbW16QDj0J6qQyWZ .marker{fill:#333333;stroke:#333333;}#mermaid-svg-gbW16QDj0J6qQyWZ .marker.cross{stroke:#333333;}#mermaid-svg-gbW16QDj0J6qQyWZ svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-gbW16QDj0J6qQyWZ p{margin:0;}#mermaid-svg-gbW16QDj0J6qQyWZ .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-gbW16QDj0J6qQyWZ .cluster-label text{fill:#333;}#mermaid-svg-gbW16QDj0J6qQyWZ .cluster-label span{color:#333;}#mermaid-svg-gbW16QDj0J6qQyWZ .cluster-label span p{background-color:transparent;}#mermaid-svg-gbW16QDj0J6qQyWZ .label text,#mermaid-svg-gbW16QDj0J6qQyWZ span{fill:#333;color:#333;}#mermaid-svg-gbW16QDj0J6qQyWZ .node rect,#mermaid-svg-gbW16QDj0J6qQyWZ .node circle,#mermaid-svg-gbW16QDj0J6qQyWZ .node ellipse,#mermaid-svg-gbW16QDj0J6qQyWZ .node polygon,#mermaid-svg-gbW16QDj0J6qQyWZ .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-gbW16QDj0J6qQyWZ .rough-node .label text,#mermaid-svg-gbW16QDj0J6qQyWZ .node .label text,#mermaid-svg-gbW16QDj0J6qQyWZ .image-shape .label,#mermaid-svg-gbW16QDj0J6qQyWZ .icon-shape .label{text-anchor:middle;}#mermaid-svg-gbW16QDj0J6qQyWZ .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-gbW16QDj0J6qQyWZ .rough-node .label,#mermaid-svg-gbW16QDj0J6qQyWZ .node .label,#mermaid-svg-gbW16QDj0J6qQyWZ .image-shape .label,#mermaid-svg-gbW16QDj0J6qQyWZ .icon-shape .label{text-align:center;}#mermaid-svg-gbW16QDj0J6qQyWZ .node.clickable{cursor:pointer;}#mermaid-svg-gbW16QDj0J6qQyWZ .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-gbW16QDj0J6qQyWZ .arrowheadPath{fill:#333333;}#mermaid-svg-gbW16QDj0J6qQyWZ .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-gbW16QDj0J6qQyWZ .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-gbW16QDj0J6qQyWZ .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-gbW16QDj0J6qQyWZ .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-gbW16QDj0J6qQyWZ .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-gbW16QDj0J6qQyWZ .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-gbW16QDj0J6qQyWZ .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-gbW16QDj0J6qQyWZ .cluster text{fill:#333;}#mermaid-svg-gbW16QDj0J6qQyWZ .cluster span{color:#333;}#mermaid-svg-gbW16QDj0J6qQyWZ div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-gbW16QDj0J6qQyWZ .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-gbW16QDj0J6qQyWZ rect.text{fill:none;stroke-width:0;}#mermaid-svg-gbW16QDj0J6qQyWZ .icon-shape,#mermaid-svg-gbW16QDj0J6qQyWZ .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-gbW16QDj0J6qQyWZ .icon-shape p,#mermaid-svg-gbW16QDj0J6qQyWZ .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-gbW16QDj0J6qQyWZ .icon-shape .label rect,#mermaid-svg-gbW16QDj0J6qQyWZ .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-gbW16QDj0J6qQyWZ .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-gbW16QDj0J6qQyWZ .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-gbW16QDj0J6qQyWZ :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 存在
不存在
email_verified且匹配
全新
收到上游ID Token
验签+校验iss
计算(iss,sub)唯一键
外部身份已存在?
取关联内部用户
可账号链接?
挂到现有用户
JIT建号+创建外部身份+绑定租户
应用租户claim映射→内部角色
建会话, 签发下游token

  • JIT(Just-In-Time)Provisioning :登录时被动建号,从 claims 取 email/name,建号即钉死来源租户
  • SCIM 预置 :大客户常要求 IdP 通过 SCIM 协议提前同步用户增删改,以支持「员工离职即时禁用」。JIT 是被动建,SCIM 是主动同步。

5.4 Claim 映射:按租户隔离的规则

不同 IdP 的角色 claim 千差万别,需每租户一套映射规则,配置化、不改代码:

复制代码
租户bytedance: groups含"admin-guid" → 内部角色 tenant_admin
租户bank:      role=="manager"      → 内部角色 tenant_admin

5.5 账号链接:多 IdP 指向同一真人

同一人从公司 SSO 和 Google 两个 IdP 登入,(iss,sub) 不同会建成两个用户。账号链接把多个外部身份挂到同一内部用户下。安全红线:绝不能仅凭未验证 email 自动链接 (账号接管漏洞)------必须 email_verified=true 或用户主动确认。


六、运行期会话生命周期:刷新与登出还要不要找 IdP

理解这一节的钥匙是:代理模式存在上下游两层独立会话。
#mermaid-svg-XtmkuplTJsyDnOi0{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-XtmkuplTJsyDnOi0 .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-XtmkuplTJsyDnOi0 .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-XtmkuplTJsyDnOi0 .error-icon{fill:#552222;}#mermaid-svg-XtmkuplTJsyDnOi0 .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-XtmkuplTJsyDnOi0 .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-XtmkuplTJsyDnOi0 .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-XtmkuplTJsyDnOi0 .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-XtmkuplTJsyDnOi0 .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-XtmkuplTJsyDnOi0 .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-XtmkuplTJsyDnOi0 .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-XtmkuplTJsyDnOi0 .marker{fill:#333333;stroke:#333333;}#mermaid-svg-XtmkuplTJsyDnOi0 .marker.cross{stroke:#333333;}#mermaid-svg-XtmkuplTJsyDnOi0 svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-XtmkuplTJsyDnOi0 p{margin:0;}#mermaid-svg-XtmkuplTJsyDnOi0 .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-XtmkuplTJsyDnOi0 .cluster-label text{fill:#333;}#mermaid-svg-XtmkuplTJsyDnOi0 .cluster-label span{color:#333;}#mermaid-svg-XtmkuplTJsyDnOi0 .cluster-label span p{background-color:transparent;}#mermaid-svg-XtmkuplTJsyDnOi0 .label text,#mermaid-svg-XtmkuplTJsyDnOi0 span{fill:#333;color:#333;}#mermaid-svg-XtmkuplTJsyDnOi0 .node rect,#mermaid-svg-XtmkuplTJsyDnOi0 .node circle,#mermaid-svg-XtmkuplTJsyDnOi0 .node ellipse,#mermaid-svg-XtmkuplTJsyDnOi0 .node polygon,#mermaid-svg-XtmkuplTJsyDnOi0 .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-XtmkuplTJsyDnOi0 .rough-node .label text,#mermaid-svg-XtmkuplTJsyDnOi0 .node .label text,#mermaid-svg-XtmkuplTJsyDnOi0 .image-shape .label,#mermaid-svg-XtmkuplTJsyDnOi0 .icon-shape .label{text-anchor:middle;}#mermaid-svg-XtmkuplTJsyDnOi0 .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-XtmkuplTJsyDnOi0 .rough-node .label,#mermaid-svg-XtmkuplTJsyDnOi0 .node .label,#mermaid-svg-XtmkuplTJsyDnOi0 .image-shape .label,#mermaid-svg-XtmkuplTJsyDnOi0 .icon-shape .label{text-align:center;}#mermaid-svg-XtmkuplTJsyDnOi0 .node.clickable{cursor:pointer;}#mermaid-svg-XtmkuplTJsyDnOi0 .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-XtmkuplTJsyDnOi0 .arrowheadPath{fill:#333333;}#mermaid-svg-XtmkuplTJsyDnOi0 .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-XtmkuplTJsyDnOi0 .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-XtmkuplTJsyDnOi0 .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-XtmkuplTJsyDnOi0 .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-XtmkuplTJsyDnOi0 .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-XtmkuplTJsyDnOi0 .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-XtmkuplTJsyDnOi0 .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-XtmkuplTJsyDnOi0 .cluster text{fill:#333;}#mermaid-svg-XtmkuplTJsyDnOi0 .cluster span{color:#333;}#mermaid-svg-XtmkuplTJsyDnOi0 div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-XtmkuplTJsyDnOi0 .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-XtmkuplTJsyDnOi0 rect.text{fill:none;stroke-width:0;}#mermaid-svg-XtmkuplTJsyDnOi0 .icon-shape,#mermaid-svg-XtmkuplTJsyDnOi0 .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-XtmkuplTJsyDnOi0 .icon-shape p,#mermaid-svg-XtmkuplTJsyDnOi0 .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-XtmkuplTJsyDnOi0 .icon-shape .label rect,#mermaid-svg-XtmkuplTJsyDnOi0 .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-XtmkuplTJsyDnOi0 .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-XtmkuplTJsyDnOi0 .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-XtmkuplTJsyDnOi0 :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 认证那刻关联
上游会话

真实IdP ↔ 代理 (代理是RP)
下游会话

代理 ↔ 应用 (代理是OP)

6.1 Token 刷新:默认不碰真实 IdP

应用持有的是你代理签发 的 token,过期后拿你的 refresh token 到你代理的 token_endpoint 换新------用你自己私钥签,与真实 IdP 无关。代理在登录那刻已把外部身份固化成内部会话,后续活跃度无需惊动上游。这也是代理模式的性能优势:上游 IdP 不被高频打扰。

6.2 登出:彻底登出往往必须找真实 IdP

登出的陷阱:你只清下游会话,但上游真实 IdP 的会话还在 ,用户回到登录页会被 IdP 静默登回,造成「假登出」。
#mermaid-svg-RjmACiEREZGfqhla{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-RjmACiEREZGfqhla .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-RjmACiEREZGfqhla .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-RjmACiEREZGfqhla .error-icon{fill:#552222;}#mermaid-svg-RjmACiEREZGfqhla .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-RjmACiEREZGfqhla .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-RjmACiEREZGfqhla .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-RjmACiEREZGfqhla .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-RjmACiEREZGfqhla .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-RjmACiEREZGfqhla .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-RjmACiEREZGfqhla .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-RjmACiEREZGfqhla .marker{fill:#333333;stroke:#333333;}#mermaid-svg-RjmACiEREZGfqhla .marker.cross{stroke:#333333;}#mermaid-svg-RjmACiEREZGfqhla svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-RjmACiEREZGfqhla p{margin:0;}#mermaid-svg-RjmACiEREZGfqhla .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-RjmACiEREZGfqhla .cluster-label text{fill:#333;}#mermaid-svg-RjmACiEREZGfqhla .cluster-label span{color:#333;}#mermaid-svg-RjmACiEREZGfqhla .cluster-label span p{background-color:transparent;}#mermaid-svg-RjmACiEREZGfqhla .label text,#mermaid-svg-RjmACiEREZGfqhla span{fill:#333;color:#333;}#mermaid-svg-RjmACiEREZGfqhla .node rect,#mermaid-svg-RjmACiEREZGfqhla .node circle,#mermaid-svg-RjmACiEREZGfqhla .node ellipse,#mermaid-svg-RjmACiEREZGfqhla .node polygon,#mermaid-svg-RjmACiEREZGfqhla .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-RjmACiEREZGfqhla .rough-node .label text,#mermaid-svg-RjmACiEREZGfqhla .node .label text,#mermaid-svg-RjmACiEREZGfqhla .image-shape .label,#mermaid-svg-RjmACiEREZGfqhla .icon-shape .label{text-anchor:middle;}#mermaid-svg-RjmACiEREZGfqhla .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-RjmACiEREZGfqhla .rough-node .label,#mermaid-svg-RjmACiEREZGfqhla .node .label,#mermaid-svg-RjmACiEREZGfqhla .image-shape .label,#mermaid-svg-RjmACiEREZGfqhla .icon-shape .label{text-align:center;}#mermaid-svg-RjmACiEREZGfqhla .node.clickable{cursor:pointer;}#mermaid-svg-RjmACiEREZGfqhla .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-RjmACiEREZGfqhla .arrowheadPath{fill:#333333;}#mermaid-svg-RjmACiEREZGfqhla .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-RjmACiEREZGfqhla .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-RjmACiEREZGfqhla .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-RjmACiEREZGfqhla .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-RjmACiEREZGfqhla .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-RjmACiEREZGfqhla .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-RjmACiEREZGfqhla .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-RjmACiEREZGfqhla .cluster text{fill:#333;}#mermaid-svg-RjmACiEREZGfqhla .cluster span{color:#333;}#mermaid-svg-RjmACiEREZGfqhla div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-RjmACiEREZGfqhla .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-RjmACiEREZGfqhla rect.text{fill:none;stroke-width:0;}#mermaid-svg-RjmACiEREZGfqhla .icon-shape,#mermaid-svg-RjmACiEREZGfqhla .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-RjmACiEREZGfqhla .icon-shape p,#mermaid-svg-RjmACiEREZGfqhla .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-RjmACiEREZGfqhla .icon-shape .label rect,#mermaid-svg-RjmACiEREZGfqhla .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-RjmACiEREZGfqhla .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-RjmACiEREZGfqhla .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-RjmACiEREZGfqhla :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 单应用登出, 保留SSO
彻底登出/合规
用户点登出
①清下游会话
是否需清上游?
本地登出完成

不碰真实IdP
②重定向到真实IdP的

end_session_endpoint
真实IdP清除会话
彻底登出完成

  • 本地登出:只清下游,不碰 IdP,用户可被静默登回。适合「退出此应用,公司 SSO 仍在」。
  • 全局登出(Single Logout) :清下游 + 调真实 IdP 的 end_session_endpoint这一步必须请求真实 IdP。
  • Back-Channel Logout:用户在 IdP 端注销时,IdP 主动通知代理清下游(IdP 推、代理收)。

6.3 极简模式:认证即用完即弃

一种合法且常见的设计:只取 ID Token、验签、建内部会话,此后主流程完全脱离 IdP。 ID Token 本就是「认证事件快照」,这是它的标准用法。甚至可只取 ID Token、丢弃 access/refresh token------若不调 IdP 的 API,它们对你无用,丢掉更干净。

能脱钩的: 会话维持、token 刷新、应用鉴权、属性读取(用登录快照)。

脱钩的代价: 快照之后世界会变,而你不再看 IdP 就感知不到------用户被禁用/离职无法实时吊销、做不到全局登出、属性更新滞后到下次登录。

6.4 折中:平时脱钩,关键点保留细线

#mermaid-svg-vZJ5sMeqIerMg7oB{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-vZJ5sMeqIerMg7oB .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-vZJ5sMeqIerMg7oB .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-vZJ5sMeqIerMg7oB .error-icon{fill:#552222;}#mermaid-svg-vZJ5sMeqIerMg7oB .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-vZJ5sMeqIerMg7oB .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-vZJ5sMeqIerMg7oB .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-vZJ5sMeqIerMg7oB .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-vZJ5sMeqIerMg7oB .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-vZJ5sMeqIerMg7oB .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-vZJ5sMeqIerMg7oB .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-vZJ5sMeqIerMg7oB .marker{fill:#333333;stroke:#333333;}#mermaid-svg-vZJ5sMeqIerMg7oB .marker.cross{stroke:#333333;}#mermaid-svg-vZJ5sMeqIerMg7oB svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-vZJ5sMeqIerMg7oB p{margin:0;}#mermaid-svg-vZJ5sMeqIerMg7oB .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-vZJ5sMeqIerMg7oB .cluster-label text{fill:#333;}#mermaid-svg-vZJ5sMeqIerMg7oB .cluster-label span{color:#333;}#mermaid-svg-vZJ5sMeqIerMg7oB .cluster-label span p{background-color:transparent;}#mermaid-svg-vZJ5sMeqIerMg7oB .label text,#mermaid-svg-vZJ5sMeqIerMg7oB span{fill:#333;color:#333;}#mermaid-svg-vZJ5sMeqIerMg7oB .node rect,#mermaid-svg-vZJ5sMeqIerMg7oB .node circle,#mermaid-svg-vZJ5sMeqIerMg7oB .node ellipse,#mermaid-svg-vZJ5sMeqIerMg7oB .node polygon,#mermaid-svg-vZJ5sMeqIerMg7oB .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-vZJ5sMeqIerMg7oB .rough-node .label text,#mermaid-svg-vZJ5sMeqIerMg7oB .node .label text,#mermaid-svg-vZJ5sMeqIerMg7oB .image-shape .label,#mermaid-svg-vZJ5sMeqIerMg7oB .icon-shape .label{text-anchor:middle;}#mermaid-svg-vZJ5sMeqIerMg7oB .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-vZJ5sMeqIerMg7oB .rough-node .label,#mermaid-svg-vZJ5sMeqIerMg7oB .node .label,#mermaid-svg-vZJ5sMeqIerMg7oB .image-shape .label,#mermaid-svg-vZJ5sMeqIerMg7oB .icon-shape .label{text-align:center;}#mermaid-svg-vZJ5sMeqIerMg7oB .node.clickable{cursor:pointer;}#mermaid-svg-vZJ5sMeqIerMg7oB .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-vZJ5sMeqIerMg7oB .arrowheadPath{fill:#333333;}#mermaid-svg-vZJ5sMeqIerMg7oB .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-vZJ5sMeqIerMg7oB .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-vZJ5sMeqIerMg7oB .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-vZJ5sMeqIerMg7oB .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-vZJ5sMeqIerMg7oB .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-vZJ5sMeqIerMg7oB .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-vZJ5sMeqIerMg7oB .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-vZJ5sMeqIerMg7oB .cluster text{fill:#333;}#mermaid-svg-vZJ5sMeqIerMg7oB .cluster span{color:#333;}#mermaid-svg-vZJ5sMeqIerMg7oB div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-vZJ5sMeqIerMg7oB .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-vZJ5sMeqIerMg7oB rect.text{fill:none;stroke-width:0;}#mermaid-svg-vZJ5sMeqIerMg7oB .icon-shape,#mermaid-svg-vZJ5sMeqIerMg7oB .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-vZJ5sMeqIerMg7oB .icon-shape p,#mermaid-svg-vZJ5sMeqIerMg7oB .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-vZJ5sMeqIerMg7oB .icon-shape .label rect,#mermaid-svg-vZJ5sMeqIerMg7oB .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-vZJ5sMeqIerMg7oB .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-vZJ5sMeqIerMg7oB .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-vZJ5sMeqIerMg7oB :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 是





需实时响应IdP侧禁用/离职?
不宜完全脱钩
需全局登出/合规?
登出保留end_session
要调IdP API/拉最新属性?
保留access/refresh token
✅完全脱钩可行

若想脱钩的轻量又不放弃安全,用三个「IdP 主动推、你被动收」的机制补缺口,主流程仍脱钩:

机制 作用 是否需主动连 IdP
短 token 寿命 缩小吊销滞后窗口 否,纯内部
Back-Channel Logout IdP 注销时推送通知清会话 否,IdP 推给你
SCIM 反预置 IdP 主动同步禁用 否,IdP 推给你

七、安全红线总览

身份代理最大的风险不是登不上,而是租户间身份越界。以下红线缺一不可:

红线 不做的后果
上游令牌按租户校验 iss(等于该租户固定 issuer) 接受别家 OP 令牌
校验 aud 等于你注册的 client_id 令牌挪用、重放
公钥按租户隔离,严禁跨租户验签 IdP 混淆攻击(Mix-Up)
唯一键用 (iss, sub) 联合 跨租户身份碰撞
外部身份钉死来源租户、会话绑定租户 越权访问其他租户数据
账号链接需 email_verified 或用户确认 账号接管
claim 映射规则按租户隔离 权限错配
离职/禁用同步(SCIM 或回查) 离职员工仍可登入
全程强制 HTTPS 端点地址被中间人篡改

IdP 混淆攻击的具象

#mermaid-svg-vygwJLbT0WNKFED9{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-vygwJLbT0WNKFED9 .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-vygwJLbT0WNKFED9 .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-vygwJLbT0WNKFED9 .error-icon{fill:#552222;}#mermaid-svg-vygwJLbT0WNKFED9 .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-vygwJLbT0WNKFED9 .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-vygwJLbT0WNKFED9 .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-vygwJLbT0WNKFED9 .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-vygwJLbT0WNKFED9 .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-vygwJLbT0WNKFED9 .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-vygwJLbT0WNKFED9 .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-vygwJLbT0WNKFED9 .marker{fill:#333333;stroke:#333333;}#mermaid-svg-vygwJLbT0WNKFED9 .marker.cross{stroke:#333333;}#mermaid-svg-vygwJLbT0WNKFED9 svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-vygwJLbT0WNKFED9 p{margin:0;}#mermaid-svg-vygwJLbT0WNKFED9 .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-vygwJLbT0WNKFED9 .cluster-label text{fill:#333;}#mermaid-svg-vygwJLbT0WNKFED9 .cluster-label span{color:#333;}#mermaid-svg-vygwJLbT0WNKFED9 .cluster-label span p{background-color:transparent;}#mermaid-svg-vygwJLbT0WNKFED9 .label text,#mermaid-svg-vygwJLbT0WNKFED9 span{fill:#333;color:#333;}#mermaid-svg-vygwJLbT0WNKFED9 .node rect,#mermaid-svg-vygwJLbT0WNKFED9 .node circle,#mermaid-svg-vygwJLbT0WNKFED9 .node ellipse,#mermaid-svg-vygwJLbT0WNKFED9 .node polygon,#mermaid-svg-vygwJLbT0WNKFED9 .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-vygwJLbT0WNKFED9 .rough-node .label text,#mermaid-svg-vygwJLbT0WNKFED9 .node .label text,#mermaid-svg-vygwJLbT0WNKFED9 .image-shape .label,#mermaid-svg-vygwJLbT0WNKFED9 .icon-shape .label{text-anchor:middle;}#mermaid-svg-vygwJLbT0WNKFED9 .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-vygwJLbT0WNKFED9 .rough-node .label,#mermaid-svg-vygwJLbT0WNKFED9 .node .label,#mermaid-svg-vygwJLbT0WNKFED9 .image-shape .label,#mermaid-svg-vygwJLbT0WNKFED9 .icon-shape .label{text-align:center;}#mermaid-svg-vygwJLbT0WNKFED9 .node.clickable{cursor:pointer;}#mermaid-svg-vygwJLbT0WNKFED9 .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-vygwJLbT0WNKFED9 .arrowheadPath{fill:#333333;}#mermaid-svg-vygwJLbT0WNKFED9 .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-vygwJLbT0WNKFED9 .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-vygwJLbT0WNKFED9 .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-vygwJLbT0WNKFED9 .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-vygwJLbT0WNKFED9 .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-vygwJLbT0WNKFED9 .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-vygwJLbT0WNKFED9 .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-vygwJLbT0WNKFED9 .cluster text{fill:#333;}#mermaid-svg-vygwJLbT0WNKFED9 .cluster span{color:#333;}#mermaid-svg-vygwJLbT0WNKFED9 div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-vygwJLbT0WNKFED9 .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-vygwJLbT0WNKFED9 rect.text{fill:none;stroke-width:0;}#mermaid-svg-vygwJLbT0WNKFED9 .icon-shape,#mermaid-svg-vygwJLbT0WNKFED9 .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-vygwJLbT0WNKFED9 .icon-shape p,#mermaid-svg-vygwJLbT0WNKFED9 .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-vygwJLbT0WNKFED9 .icon-shape .label rect,#mermaid-svg-vygwJLbT0WNKFED9 .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-vygwJLbT0WNKFED9 .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-vygwJLbT0WNKFED9 .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-vygwJLbT0WNKFED9 :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 未校验
严格校验
攻击者持A租户合法令牌
在B租户流程中冒用
校验iss==B租户固定issuer?
把A身份当B身份, 越权
iss是A的≠B → 拒绝

这正是「每租户独立校验固定 issuer + 独立密钥」的根本理由------把「我以为在和哪个租户 OP 通信」与「令牌实际由哪个 OP 签发」锁死成同一身份。


八、设计决策速查

决策点 推荐 备注
角色定位 对应用当 OP,对 IdP 当 RP 身份代理模式
下游 issuer 每租户独立(路径区分) 隔离性强,仿 Azure AD
家域发现 子域名+邮箱域名为主,显式选择兜底 按客户规模组合
身份唯一键 (iss, sub) 联合 绝不只用 sub
建号方式 JIT 默认,大客户上 SCIM 离职即时禁用需 SCIM
Token 刷新 下游自签,默认不碰 IdP 性能与解耦
登出 本地登出默认;合规场景全局登出 全局需 end_session
会话安全 短 token + back-channel logout 平时脱钩,关键点补线

结语

多租户身份代理架构的复杂度,从不在 OIDC 协议本身,而在「一套系统如何安全地与 N 个彼此独立的身份提供方共处,同时让业务应用毫无感知」。理清后,整个体系脉络清晰:

  • 架构上,代理对上游当 RP 收身份、对下游当 OP 发身份,吸收异构性、输出同构身份;
  • 流程上,家域发现前置路由,双跳认证完成联合登录,代理用自己的私钥重新签发令牌;
  • 映射上 ,以 (iss, sub) 为地基,JIT/SCIM 建号,按租户隔离 claim 规则,账号链接谨防接管;
  • 运行期,刷新默认脱离 IdP、登出按需联动,可做到「认证即用完即弃」,也可用 back-channel/SCIM 补上吊销缺口;
  • 安全上,每租户固定 issuer 严格校验、密钥隔离、会话绑定租户,以此防住 IdP 混淆与租户越界。

一句话收束:多租户身份的安全,从来不是假设出来的,而是在每一次登录、对每一个租户、用其固定的 issuer 与独立的密钥、以 (iss, sub) 为锚,显式校验与隔离出来的。 身份代理的精髓,就是把这份「显式」收敛到一个集中的、边界清晰的中间层。

相关推荐
Hello:CodeWorld2 小时前
Dify 从入门到实战:部署、模型对接与企业级 AI 应用开发全教程
人工智能·python·架构·ai编程
ihuyigui3 小时前
国际商超零售短信接口
大数据·前端·后端·架构·零售
ting94520003 小时前
Fundraisly 融资定向 AI 智能体全栈技术深度剖析
人工智能·架构
段一凡-华北理工大学3 小时前
工业领域的Hadoop架构学习~系列文章20:故障诊断与根因分析 - 从表象到本质的智能推理
大数据·人工智能·hadoop·学习·架构·高炉炼铁·工业智能体
凌云拓界3 小时前
状态机与思考循环 ——CogitoAgent开发实战(一)
javascript·人工智能·架构·node.js·设计规范
商业模式源码开发4 小时前
跨店积分抵现模式深度解析:本地生活增值闭环的商业架构与落地方法论
架构·异业联盟
这个DBA有点耶5 小时前
时序数据库深度对比:2026 年主流 TSDB 架构演进与选型指南
数据库·sql·云原生·架构·运维开发·时序数据库
卖芒果的潇洒农民6 小时前
Work FW-HW架构
架构
caimouse6 小时前
Reactos 第 5 章 进程与线程 — 5.1 概述
c语言·windows·架构
该昵称用户已存在6 小时前
能源数字化架构手记:MyEMS 数据建模引擎的模块化拆分与接口治理
架构·能源