OpenID Connect(OIDC)深度解析:OAuth 2.0 之上的身份认证层
为什么需要 OIDC?
OAuth 2.0 解决了「授权」问题——应用如何代表用户访问资源。但它留下了一个关键空白:身份认证。
想象一下这个场景:用户登录了某电商平台,获得了一个 Access Token。平台后端用这个 Token 调用了支付服务,支付服务返回了用户信息。但支付服务怎么知道这个用户是张三还是李四?OAuth 2.0 的 Access Token 只携带权限信息,不携带用户身份信息。
这就是 OpenID Connect(OIDC)要解决的问题。OIDC 是 OAuth 2.0 之上的身份认证层,由 OpenID Foundation 维护,核心规范定义在 RFC 7519(JWT)、RFC 7591(动态客户端注册)以及 OIDC Core 1.0 规范中。
OIDC 的核心组件
OIDC 体系包含四个核心概念:
┌─────────────────────────────────────────────────────────────────────┐
│ OIDC 核心组件架构 │
├──────────────────┬──────────────────────────────────────────────────┤
│ 角色 │ 说明 │
├──────────────────┼──────────────────────────────────────────────────┤
│ End-User │ 最终用户(需要登录的人) │
│ Relying Party │ 依赖方,即使用 OIDC 的服务(App/网站) │
│ OpenID Provider │ 身份提供者(OP),负责认证和签发 Token │
│ User Agent │ 浏览器或移动客户端 │
└──────────────────┴──────────────────────────────────────────────────┘ID Token:OIDC 的核心创新
OAuth 2.0 只有 Access Token(权限凭证)和 Refresh Token(续期凭证)。OIDC 引入了第三个角色:ID Token。
ID Token 是一个 Signed JWT,包含以下核心声明:
| 声明 | 必填 | 说明 |
|---|---|---|
| iss | ✓ | Issuer,身份提供者标识符 |
| sub | ✓ | Subject,用户的唯一标识 |
| aud | ✓ | Audience,预期接收方(RP 的 client_id) |
| exp | ✓ | 过期时间 |
| iat | ✓ | 签发时间 |
| auth_time | 条件必填 | 用户实际认证时间 |
| nonce | 条件必填 | 认证请求中的随机数,防止重放 |
| at_hash | 条件必填 | Access Token 的哈希,绑定两者 |
// ID Token 示例(JWT 格式)
{
"iss": "https://accounts.example.com",
"sub": "248289761001",
"aud": "s6BhdRkqt3",
"exp": 1311281970,
"iat": 1311280970,
"auth_time": 1311280870,
"nonce": "n-0S6_WzA2Mj",
"at_hash": "SILVuk7KpGzfH2eQL3ZIhw",
"name": "John Doe",
"email": "john@example.com",
"email_verified": true
}UserInfo 端点:补充用户属性
ID Token 只携带少量声明(最多 10 个)。如果需要更多用户信息,OIDC 定义了 UserInfo 端点——一个独立的 HTTPS 接口,RP 携带 Access Token 去查询完整用户资料。
# 调用 UserInfo 端点
GET /userinfo HTTP/1.1
Host: accounts.example.com
Authorization: Bearer [Access Token]
# 响应
{
"sub": "248289761001",
"name": "John Doe",
"email": "john@example.com",
"email_verified": true,
"picture": "https://example.com/john.jpg"
}认证流程:授权码流程(Authorization Code Flow)
这是 OIDC 最核心、最常用的流程。完整的交互序列如下:
1. 用户访问 RP(第三方应用)
2. RP 重定向用户到 OP 的 /authorize 端点:
GET /authorize?
response_type=code
client_id=s6BhdRkqt3
redirect_uri=https://rp.example.com/callback
scope=openid profile email
nonce=n-0S6_WzA2Mj
state=xYz123
3. 用户在 OP 处登录(输入账号密码,或通过 MFA)
4. OP 重定向回 RP,附带授权码:
https://rp.example.com/callback?code=SplxlOBeZQQYbYS6WxSbIA
&state=xYz123
5. RP 用授权码换取 Token(后端到后端):
POST /token HTTP/1.1
Content-Type: application/x-www-form-urlencoded
grant_type=authorization_code
&code=SplxlOBeZQQYbYS6WxSbIA
&redirect_uri=https://rp.example.com/callback
&client_id=s6BhdRkqt3
&client_secret=...
&nonce=n-0S6_WzA2Mj
6. OP 返回 JSON 响应:
{
"access_token": "SlAV32hkKG",
"token_type": "Bearer",
"expires_in": 3600,
"refresh_token": "tGzv3JOkF0XG5Qx2TlKWIA",
"id_token": "eyJhbGciOiJSUzI1NiIs...",
"scope": "openid profile email"
}
7. RP 验证 id_token:
- 检查签名(使用 OP 的公钥,可从 /.well-known/jwks.json 获取)
- 验证 iss 符合预期
- 验证 aud 包含自己的 client_id
- 验证 exp 未过期
- 验证 nonce 与请求时一致
- 如携带 at_hash,验证 Access Token 匹配
8. (可选)RP 调用 UserInfo 端点获取完整用户资料关键安全设计:state 参数
state 参数用于防止 CSRF 攻击。OP 响应时必须原样返回该值,RP 需验证两者一致。
关键安全设计:nonce 参数
nonce 防止 ID Token 重放攻击。每次认证请求 OP 必须返回相同的 nonce,RP 验证匹配。
其他认证流程
隐式流程(Implicit Flow)—— 已废弃
GET /authorize?response_type=token&id_token=xxx&state=...早期用于 SPA(单页应用),将 Token 直接暴露在 URL fragment 中。已被 RFC 9207 标记为 deprecated,不应在新系统中使用。
混合流程(Hybrid Flow)
支持同时获取 Code 和 Token,适合需要部分即时响应的场景。但由于实现复杂且安全风险较高,当前也不推荐。
设备授权流程(Device Authorization Grant)
用于无键盘设备(电视、IoT):
1. 设备请求授权:
POST /device_authorization
→ 返回 device_code + user_code(如 ABCD-1234)+ verification_uri
2. 用户在其他设备上打开 verification_uri,输入 user_code
3. 设备轮询 /token,直到用户授权完成发现机制:.well-known/openid-configuration
OIDC 定义了标准化的发现端点。OP 在 .well-known/openid-configuration 路径下暴露所有端点 URI 和协议能力:
{
"issuer": "https://accounts.example.com",
"authorization_endpoint": "https://accounts.example.com/authorize",
"token_endpoint": "https://accounts.example.com/token",
"userinfo_endpoint": "https://accounts.example.com/userinfo",
"jwks_uri": "https://accounts.example.com/.well-known/jwks.json",
"registration_endpoint": "https://accounts.example.com/register",
"scopes_supported": ["openid", "profile", "email", "phone", "address"],
"response_types_supported": ["code", "token", "id_token",
"code token", "code id_token",
"id_token token", "code id_token token"],
"grant_types_supported": ["authorization_code", "refresh_token"],
"subject_types_supported": ["public"],
"id_token_signing_alg_values_supported": ["RS256", "ES256"]
}RP 通过调用此端点自动发现所有必要信息,无需硬编码配置。
Scopes 与 Claims
标准 Scope
| Scope | 含义 | 常见 Claims |
|---|---|---|
openid | 必需,标识 OIDC 请求 | - |
profile | 基础用户资料 | name, given_name, family_name, picture |
email | 邮箱地址 | email, email_verified |
phone | 电话号码 | phone_number, phone_number_verified |
address | 地址信息 | formatted, street_address, locality |
自定义 Scope
OP 可定义私有 scope,如 read:orders、write:payments。建议最小化授权——只请求需要的 scope。
OIDC 标准 Claims
| Claim | 数据类型 | 描述 |
|---|---|---|
| sub | string | 用户唯一标识(Opaque,不应泄露) |
| name | string | 全名 |
| given_name | string | 名字 |
| family_name | string | 姓氏 |
| middle_name | string | 中间名 |
| nickname | string | 昵称 |
| preferred_username | string | 偏好用户名 |
| profile | URI | 用户资料页 URL |
| picture | URI | 头像 URL |
| website | URI | 个人网站 |
| string | 邮箱 | |
| email_verified | boolean | 邮箱是否已验证 |
| gender | string | 性别 |
| birthdate | string | 出生日期(YYYY-MM-DD) |
| zoneinfo | string | 时区(IANA 格式) |
| locale | string | 语言(BCP 47 格式) |
| phone_number | string | 电话号码 |
| phone_number_verified | boolean | 电话是否已验证 |
| address | object | 地址对象 |
| updated_at | number | 资料最后更新时间 |
与 OAuth 2.0 的核心区别
| 维度 | OAuth 2.0 | OIDC |
|---|---|---|
| 核心目的 | 授权(Access Token) | 认证(ID Token) |
| Token 类型 | Access Token | Access Token + ID Token + Refresh Token |
| 用户信息 | 不定义 | 通过 ID Token + UserInfo 端点 |
| 发现机制 | 无标准 | .well-known/openid-configuration |
| 会话管理 | 无标准 | Front/Back-channel Logout |
| 适用范围 | 资源授权 | 身份认证 |
安全最佳实践
1. 始终使用 PKCE
即使你的应用是"机密客户端"(有后端服务器),也必须使用 PKCE。2019 年 OWASP 将 PKCE 标记为所有 OAuth 2.0 实现的必备措施。
2. 验证 ID Token 签名
使用 OP 发布的 JWKS(JSON Web Key Set)验证签名:
- 不支持 RS256 时,拒绝 Token
- 不要硬编码公钥——定期刷新 JWKS 缓存
- 注意 JWKS 中可能包含多个 key_id,正确匹配
3. 验证 nonce
每次认证请求生成唯一的随机 nonce,验证 ID Token 中的 nonce 字段完全匹配。防止 ID Token 重放攻击。
4. 使用 Authorization Code + PKCE 流程
隐式流程和 Hybrid 流程均已 deprecated。新系统只应使用 Authorization Code Flow(配合 PKCE)。
5. 最小化 Scope 请求
只请求应用实际需要的 scope 和 claims。过度请求会降低用户体验并增加隐私风险。
6. 验证 state 参数
防止 CSRF 攻击——必须验证 OP 返回的 state 与请求时一致。
7. 检查 at_hash
当 ID Token 与 Access Token 同时返回时,验证 at_hash 确保两者绑定,防止令牌替换攻击。
国密适配:SM2 + SM3 的 OIDC 实现
OIDC 标准假设使用 RSA/ECDSA 签名和 JWT 格式。国密场景下的适配方案:
方案一:SM2 签名 JWT(SM2-JWT)
将传统 JWT 的 alg 字段替换为 SM2 相关值:
ES256→SM2WithSM3(标识符:1.2.156.10197.1.501)- RS256/ES256 签名算法替换为 SM2 签名
方案二:ID Token 使用国密证书
在 ID Token 的 jwk 字段中嵌入 SM2 公钥证书(X.509 格式),通过国密 JWKS 端点分发。
方案三:国密 TLS 隧道
使用 GM/T 0024(国密 SSL/TLS 协议)建立 OP 与 RP 之间的安全通道,ID Token 传输层使用国密算法保护。
实际挑战
- JWKS 扩展:当前 JWKS 规范(RFC 7517)仅支持 RSA/EC 密钥,SM2 密钥的表示需要扩展
- 客户端库:主流 OIDC 客户端库(auth0、okta、identityserver)对 SM2 支持有限
- 浏览器兼容性:浏览器原生不支持 SM2 签名验证,需后端代理
相关实践
- OAuth 2.1 授权框架 — 授权层基础
- SAML 2.0 与遗留 SSO — OIDC 的替代方案对比
- FIDO2 与 WebAuthn — 无密码认证的补充协议
参考
- OpenID Connect Core 1.0: https://openid.net/specs/openid-connect-core-1_0.html
- OAuth 2.0 (RFC 6749): https://datatracker.ietf.org/doc/html/rfc6749
- JWT (RFC 7519): https://datatracker.ietf.org/doc/html/rfc7519
- OAuth 2.1 Security BCP (RFC 9700): https://datatracker.ietf.org/doc/html/rfc9700
- GM/T 0024-2023《SSL VPN 技术规范》— 国密 TLS 协议
- GM/T 0015-2023《数字证书格式》— 国密 X.509 证书规范