企业 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),具体体现在四个要素上:
- ID Token:一个 JWT,包含用户的身份声明(claims),由 OP(OpenID Provider,身份提供方)签发,RP(Relying Party,依赖方/应用)验证。
- UserInfo Endpoint:一个标准的 OAuth 2.0
受保护资源,用
access_token获取更多用户属性。 - Discovery:一个标准化的元数据端点(
/.well-known/openid-configuration),让 RP 自动发现 OP 的端点、支持的算法和特性。 - 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 多了几条关键机制:
scope=openid:告诉 OP “这是 OIDC 请求,请返回 ID Token”。没有这个 scope,OP 不会签发 ID Token。nonce参数:RP 随机生成的值,OP 把它放进 ID Token 的nonceclaim。RP 验证 ID Token 时比对 nonce,防止重放攻击。- UserInfo Endpoint:从 ID Token 中拿到的
claims
可能只是基本集(
sub、iss、aud),更丰富的 profile 信息通过 UserInfo 获取。
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):
- 如果 ID Token 是加密的(JWE),先解密(使用 RP 的私钥或 client_secret)。
- 从 OP 的 JWKS 端点(
jwks_uri字段)获取签名公钥,验证 JWS 签名。 - 验证
iss与 Discovery 文档中的 issuer 完全一致。 - 验证
aud包含本 RP 的client_id。 - 验证
azp(如果有):多 audience 场景下,azp指示哪个 client 是 authorized party。 - 验证
exp未过期。 - 验证
iat(可选,但建议)。 - 验证
nonce(如果授权请求中传了)。
最常见的工程错误:
- 不验证
iss:接受任意 OP 签发的 ID Token,等于接受任意人伪造的登录请求。 - 不验证
aud:一个 OP 签发给 client A 的 ID Token 被 client B 接受——攻击者注册自己的 client,用自己 client 拿到的 ID Token 登录别人的应用。 - 不验证
nonce:攻击者截获一个 ID Token,在它过期前重放。 - 用
access_token当作 ID Token 验身份:access_token是不透明字符串,JWT 格式不是必需的——它可能不是 JWT,即便是 JWT 也不保证包含用户身份 claims。
二、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"]
}工程意义:
- JWKS URI(
jwks_uri):RP 从该端点获取 OP 的签名公钥。密钥会轮换,JWKS 中可能有多个 key(由kid区分)。RP 必须实现 JWKS 缓存和失效机制——每次都拉 JWKS 会引入不必要的延迟和单点故障。 end_session_endpoint:RP-Initiated Logout 的入口。如果这个字段不存在,OP 不支持前端登出。token_endpoint_auth_methods_supported:RP 在/token端点的认证方式。client_secret_post是放在 POST body 里,client_secret_basic是 HTTP Basic Auth,private_key_jwt是用 JWT 断言认证(安全性最高)。id_token_signing_alg_values_supported:OP 支持的签名算法。大多数 OP 支持RS256,有些也支持ES256。RP 应该拒绝使用none算法的 ID Token(即便 OP 声明支持)。
工程陷阱 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 登出的合理方案是:
- RP 调用 RP-Initiated Logout,让 OP 清除用户会话。
- RP 清除自己的本地 Session Cookie。
- 设置 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。三种主流策略:
- Domain-based
Discovery(域名发现):用户输入 email(如
user@company.com),RP 查询company.com对应的 OP。需要维护域名到 IdP 的映射表。 - IdP-initiated Login:用户先从 IdP 门户点击应用图标,IdP 生成 SAML/OIDC 断言发送给 RP。这绕过了 Discovery 问题,但用户必须先到 IdP。
- RP 展示 IdP 列表:RP 提供一个”选择登录方式”页面,列出所有支持的 IdP。用户选一个,RP 重定向到该 IdP。
对于 B2B SaaS,Domain-based Discovery 是最常见的方案——用户输入公司邮箱,系统自动路由到该公司的 IdP。实现时需要注意:如果输入的域名没有对应的 SSO 配置,要优雅地降级到用户名+密码登录。
六、小结:生产环境 OIDC 的最低检查清单
上一篇:IAM 全景:为什么这是高价值赛道 下一篇:OAuth 2.1 与 PKCE:现代授权主路径
同主题继续阅读
把当前热点继续串成多页阅读,而不是停在单篇消费。
【身份与访问控制工程】IAM 全景:为什么这是高价值赛道
从 2020 年 SolarWinds 到 2024 年 Okta 支持系统泄露,身份基础设施的安全失败反复证明一件事:IAM 不是 IT 支撑系统,而是安全架构的承重墙。本文建立现代 IAM 的全景地图——从认证协议、令牌体系、权限模型到身份治理与平台选型,给出 5 个贯穿全系列的核心问题。
【身份与访问控制工程】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) 的实际应用。
身份与访问控制工程
从 OIDC、OAuth 2.1、SAML、SCIM 到多租户权限、CIAM、PAM 与身份平台选型——系统拆解现代身份与访问控制的协议、架构与工程实践。
【身份与访问控制工程】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 在实际企业客户场景中的选型逻辑。