当一个 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) 为锚,显式校验与隔离出来的。 身份代理的精髓,就是把这份「显式」收敛到一个集中的、边界清晰的中间层。