土法炼钢兴趣小组的算法知识备份

【身份与访问控制工程】企业单点登录:OIDC 与现代 SSO

文章导航

分类入口
architecturesecurity
标签入口
#oidc#openid-connect#sso#authentication#authorization-code#pkce#id-token#jwks#rp-logout

目录

企业 SSO 的需求很简单:用户登录一次,就能访问公司内的所有应用。但实现这条需求的协议栈并不简单。OIDC(OpenID Connect)是目前的主流答案——它建立在 OAuth 2.0 的授权框架之上,添加了一层身份认证语义。问题是,大多数团队对 OIDC 的理解停留在”调一个 /authorize 拿到 id_token,验个签名就行”,而协议规范中那些”可选的”部分——Discovery、Dynamic Registration、RP-Initiated Logout、Session Management——恰恰是生产环境中的填坑主力。

本文假设你已经了解 OAuth 2.0 授权码模式的基本流程(如果不了解,先翻一遍 OAuth2 入门RFC 6749),在此基础上直接进入 OIDC 的核心机制和工程细节。

一、OIDC 在 OAuth 2.0 之上加了什么

OAuth 2.0 是授权框架,它回答的问题是”客户端能否访问某个资源”。OAuth 2.0 本身不定义”用户是谁”——它只给 access_token,不告诉你是谁在授权。

OIDC 的核心贡献是在 OAuth 2.0 的流程上增加了一个标准化的身份层(Identity Layer),具体体现在四个要素上:

  1. ID Token:一个 JWT,包含用户的身份声明(claims),由 OP(OpenID Provider,身份提供方)签发,RP(Relying Party,依赖方/应用)验证。
  2. UserInfo Endpoint:一个标准的 OAuth 2.0 受保护资源,用 access_token 获取更多用户属性。
  3. Discovery:一个标准化的元数据端点(/.well-known/openid-configuration),让 RP 自动发现 OP 的端点、支持的算法和特性。
  4. Session Management 与 Logout:定义了 RP 如何知道用户在 OP 的会话已结束,以及如何发起登出。
sequenceDiagram
    participant User as 用户浏览器
    participant RP as 应用 (RP)
    participant OP as 身份提供方 (OP)

    User->>RP: 1. 访问应用 (未登录)
    RP->>User: 2. 302 重定向到 OP /authorize
    Note over User,OP: ?response_type=code&scope=openid profile email&redirect_uri=...&state=xyz&nonce=abc&code_challenge=...

    User->>OP: 3. GET /authorize (用户登录 OP)
    OP->>User: 4. 用户认证 (可能输入密码/MFA)
    OP->>User: 5. 302 重定向回 RP redirect_uri
    Note over User,RP: ?code=AUTH_CODE&state=xyz

    User->>RP: 6. GET /callback?code=AUTH_CODE&state=xyz
    RP->>RP: 7. 校验 state
    RP->>OP: 8. POST /token (code + code_verifier)
    OP->>RP: 9. { access_token, id_token, refresh_token }

    RP->>RP: 10. 验证 id_token (签名/iss/aud/exp/nonce)
    RP->>OP: 11. GET /userinfo (Authorization: Bearer access_token)
    OP->>RP: 12. { sub, name, email, ... }

    RP->>User: 13. 设置 Session Cookie, 返回应用页面

这个流程比纯 OAuth 2.0 多了几条关键机制:

1.1 ID Token 的结构与验证规则

ID Token 是 OIDC 与纯 OAuth 2.0 最关键的区别。它是一个 JWT(详见本系列第 06 篇),但 OIDC 对 JWT 的 claims 做了明确约定(OIDC Core 1.0, Section 2)。

典型 ID Token 的 payload:

{
  "iss": "https://accounts.example.com",
  "sub": "248289761001",
  "aud": "s6BhdRkqt3",
  "exp": 1311281970,
  "iat": 1311280970,
  "nonce": "n-0S6_WzA2Mj",
  "auth_time": 1311280969,
  "acr": "urn:mace:incommon:iap:silver",
  "amr": ["pwd", "mfa"]
}

关键 claims:

Claim 含义 验证规则
iss 签发者(Issuer) 必须与 Discovery 文档中的 issuer 精确匹配(包括 https:// 和尾部没有斜杠)
sub 用户标识(Subject) 在同一个 iss 下唯一标识一个用户;不同 client 可能拿到不同的 sub(如果 OP 支持 pairwise subject)
aud 接收方(Audience) 必须包含 RP 的 client_id;如果是多 audience 的 ID Token,RP 只应接受包含自己 client_id 的 token
exp 过期时间 必须大于当前时间,建议留 5 分钟时钟偏差容差
iat 签发时间 可用于拒绝”太老”的 token(如只接受 10 分钟内签发的)
nonce 防重放随机数 RP 必须在授权请求中传 nonce,并验证 ID Token 中的 nonce 完全一致
auth_time 用户最近一次认证的时间 如果距离当前时间太久,RP 可要求重新认证(通过 max_age 参数)
acr / amr 认证上下文参考 / 认证方法参考 acr 表示认证强度等级(如 NIST 800-63 的 AAL2),amr 表示具体用了什么方式(密码/MFA/生物识别)

RP 验证 ID Token 的完整步骤OIDC Core 1.0, Section 3.1.3.7):

  1. 如果 ID Token 是加密的(JWE),先解密(使用 RP 的私钥或 client_secret)。
  2. 从 OP 的 JWKS 端点(jwks_uri 字段)获取签名公钥,验证 JWS 签名。
  3. 验证 iss 与 Discovery 文档中的 issuer 完全一致。
  4. 验证 aud 包含本 RP 的 client_id
  5. 验证 azp(如果有):多 audience 场景下,azp 指示哪个 client 是 authorized party。
  6. 验证 exp 未过期。
  7. 验证 iat(可选,但建议)。
  8. 验证 nonce(如果授权请求中传了)。

最常见的工程错误

二、OpenID Connect Discovery:互操作性的基础

Discovery(OpenID Connect Discovery 1.0)是 OIDC 被企业 SSO 广泛采用的关键原因之一。它让 RP 不需要硬编码 OP 的端点地址——只需一个 OP 的 issuer URL,就能自动发现所有需要的端点。

RP 从 {issuer}/.well-known/openid-configuration 获取 JSON 文档,例如:

{
  "issuer": "https://accounts.google.com",
  "authorization_endpoint": "https://accounts.google.com/o/oauth2/v2/auth",
  "token_endpoint": "https://oauth2.googleapis.com/token",
  "userinfo_endpoint": "https://openidconnect.googleapis.com/v1/userinfo",
  "jwks_uri": "https://www.googleapis.com/oauth2/v3/certs",
  "end_session_endpoint": "https://accounts.google.com/logout",
  "scopes_supported": ["openid", "profile", "email"],
  "response_types_supported": ["code"],
  "grant_types_supported": ["authorization_code", "refresh_token"],
  "subject_types_supported": ["public"],
  "id_token_signing_alg_values_supported": ["RS256"],
  "token_endpoint_auth_methods_supported": ["client_secret_post", "client_secret_basic"]
}

工程意义:

工程陷阱 1:Discovery 文档可能来自 CDN 缓存、反向代理或 OP 的配置错误,导致 issuer 字段与 RP 的预期不一致。RP 在验证时应使用请求时的 issuer URL 与 Discovery 文档中的 issuer 做精确字符串比对,而非信任文档本身提供的 issuer

三、RP-Initiated Logout:为什么登出这么难

SSO 登出比 SSO 登录难得多。登录时只需要一次交互:OP 认证用户,RP 拿到 ID Token。登出时需要处理多个独立的会话:用户在 OP 的会话、用户在 RP1 的会话、用户在 RP2 的会话……而且用户可能只关了浏览器标签页而不主动登出。

OIDC 定义了三种登出机制:

3.1 RP-Initiated Logout(OIDC RP-Initiated Logout 1.0

用户在 RP 点击”退出登录”时,RP 重定向用户的浏览器到 OP 的 end_session_endpoint

GET /logout?id_token_hint=eyJhbG...&post_logout_redirect_uri=https://rp.example.com/logged-out&state=abc123

OP 收到后: 1. 解析 id_token_hint,识别是哪个用户(不需要验证签名,因为这是浏览器传来的,OP 本身会有用户的会话信息)。 2. 清除用户在 OP 的会话。 3. 如果提供了 post_logout_redirect_uri(需要已在 OP 预先注册),重定向回去。

这解决了”退出 OP”,但没有解决”退出其他 RP”——用户可能同时登录了多个应用。OP 可以维护一个”该用户登录过的 RP 列表”,在登出时逐一向各 RP 发送 back-channel logout 请求(OIDC Back-Channel Logout 1.0),但这要求 RP 实现 backchannel_logout_uri 端点。

3.2 Session Management(已过时)

OIDC 最初在 Core 规范中定义了 Session Management——RP 在页面中嵌入一个隐藏 iframe,OP 在 iframe 中检查会话状态,通过 postMessage 通知 RP。这个机制依赖第三方 cookie(OP 的 cookie 需要在 RP 的 iframe 中可用),而 Safari 的 ITP(Intelligent Tracking Prevention)和 Chrome 的第三方 cookie 逐步淘汰已经让这个方案实际上不可用。

OIDC 工作组正在推动 OIDC Session Management 的替代方案,但目前还没有广泛部署。

3.3 工程取舍

对于绝大多数应用,实现 SSO 登出的合理方案是:

  1. RP 调用 RP-Initiated Logout,让 OP 清除用户会话。
  2. RP 清除自己的本地 Session Cookie。
  3. 设置 Access Token / Refresh Token 的有效期足够短(如 Access Token 5 分钟,Refresh Token 15 分钟),通过令牌自动过期来限制”登出后残留访问”的窗口。

工程陷阱 2:很多实现只清除了 RP 的 session 而没有通知 OP 登出。结果是用户”退出”了应用 A,但他在 OP 的会话还在——如果有人在那台浏览器上再访问应用 B,不会要求重新登录。

四、OIDC 的客户端类型与认证方式

OIDC 定义了两类客户端(OIDC Core 1.0, Section 2.1):

类型 能保护 client_secret 吗? 允许的授权模式 token 端点认证方式
Confidential(机密客户端) 是(后端服务器) Authorization Code, Client Credentials client_secret_post, client_secret_basic, private_key_jwt
Public(公开客户端) 否(SPA, 移动 App) Authorization Code + PKCE none(不需要,因为没有 secret)

SPA(Single Page Application)和移动 App 是典型的公开客户端——它们的代码完全暴露在客户端,client_secret 无法安全存储。对于公开客户端,PKCE 是强制要求(详见本系列第 03 篇)。

工程陷阱 3:把 SPA 当成机密客户端,在前端代码里硬编码 client_secret,然后在 /token 端点用 client_secret_post 认证。client_secret 在浏览器端不是秘密——任何人打开 DevTools 都能看到。

五、多 OP 联邦:Home Realm Discovery

当一个企业对接多个 IdP(Identity Provider,身份提供方)时——例如同时支持 Google Workspace、Azure AD 和自建 Keycloak——RP 面临一个问题:用户在登录前,RP 怎么知道该把他送到哪个 OP?

这个问题的标准名称是 Home Realm Discovery。三种主流策略:

  1. Domain-based Discovery(域名发现):用户输入 email(如 user@company.com),RP 查询 company.com 对应的 OP。需要维护域名到 IdP 的映射表。
  2. IdP-initiated Login:用户先从 IdP 门户点击应用图标,IdP 生成 SAML/OIDC 断言发送给 RP。这绕过了 Discovery 问题,但用户必须先到 IdP。
  3. RP 展示 IdP 列表:RP 提供一个”选择登录方式”页面,列出所有支持的 IdP。用户选一个,RP 重定向到该 IdP。

对于 B2B SaaS,Domain-based Discovery 是最常见的方案——用户输入公司邮箱,系统自动路由到该公司的 IdP。实现时需要注意:如果输入的域名没有对应的 SSO 配置,要优雅地降级到用户名+密码登录。

六、小结:生产环境 OIDC 的最低检查清单


上一篇IAM 全景:为什么这是高价值赛道 下一篇OAuth 2.1 与 PKCE:现代授权主路径

同主题继续阅读

把当前热点继续串成多页阅读,而不是停在单篇消费。

2026-06-13 · architecture / security

【身份与访问控制工程】IAM 全景:为什么这是高价值赛道

从 2020 年 SolarWinds 到 2024 年 Okta 支持系统泄露,身份基础设施的安全失败反复证明一件事:IAM 不是 IT 支撑系统,而是安全架构的承重墙。本文建立现代 IAM 的全景地图——从认证协议、令牌体系、权限模型到身份治理与平台选型,给出 5 个贯穿全系列的核心问题。

2026-06-13 · architecture / security

【身份与访问控制工程】OAuth 2.1 与 PKCE:现代授权主路径

OAuth 2.1 不是新协议,而是对 OAuth 2.0 的安全加固:废除 Implicit Grant 和 Resource Owner Password Grant,强制 PKCE 用于所有使用授权码模式的客户端,要求精确 redirect_uri 比对。本文从 PKCE 的密码学动机出发,拆解 OAuth 2.1 的授权码流程完整交互、Refresh Token 轮换与发送者约束、DPoP 令牌绑定,以及 DCR (Dynamic Client Registration) 和 RAR (Rich Authorization Requests) 的实际应用。

2026-04-21 · architecture / security

身份与访问控制工程

从 OIDC、OAuth 2.1、SAML、SCIM 到多租户权限、CIAM、PAM 与身份平台选型——系统拆解现代身份与访问控制的协议、架构与工程实践。

2026-06-14 · architecture / security

【身份与访问控制工程】SAML 还值得学吗:企业遗留 SSO 的现实世界

2026 年了,SAML 2.0 这个诞生于 2005 年的标准在 OIDC 的压力下看似日薄西山,但全球超过 70% 的企业 SaaS 产品仍然把 SAML SSO 放在 Enterprise 定价方案的第一行。本文拆解 SAML 2.0 的核心协议模型、SP-Initiated 和 IdP-Initiated 两种 SSO 流程、NameID 的选择策略、SAML Metadata 的互操作性工程,以及 SAML 和 OIDC 在实际企业客户场景中的选型逻辑。


By .