之前我写过一个介绍OAuth+PKCE的文章,里面说到的用户登录,就是OIDC的方式。
OpenID Connect(OIDC)是建立在 OAuth 2.0 之上的身份认证协议。OAuth 解决“应用能访问哪些资源”,OIDC 进一步解决“当前用户是谁,以及如何验证这个身份”。
官网:https://openid.net/developers/how-connect-works/
假设用户正在访问一个第三方应用:
OIDC 就是这套“委托登录”的标准化协议:用户在 IdP 完成认证,IdP 向客户端返回一个经过签名的 ID Token,客户端据此确认用户身份。
| 角色 | 含义 | 示例 |
|---|---|---|
| End-User | 需要登录的用户 | Alice |
| Client / Relying Party(RP) | 请求用户登录的应用 | 企业后台、移动 App |
| OpenID Provider(OP) | 负责认证并签发令牌的服务 | Keycloak、Auth0、Microsoft Entra ID |
| Resource Server | 保存受保护 API 资源的服务 | 用户中心 API |
| Authorization Server | OAuth 角色中的授权服务器,OIDC 中通常与 OP 合并 | IdP 的授权端点和令牌端点 |
一个 IdP 往往同时扮演 OP、Authorization Server 和部分 Resource Server 的角色,但协议概念上仍应区分它们。
openid profile email。sub、iss、aud、email。OAuth 2.0 最初解决的是授权问题。客户端可以拿着 Access Token 调用 API,但仅凭 OAuth 规范,客户端并没有一个统一、可靠的方式回答:
这个 Token 对应的用户是谁?这个登录结果是不是由指定的身份提供商签发?
早期应用常见的做法是:拿 Access Token 调用某个用户信息 API,再自行约定返回字段。这会带来:
OIDC 在 OAuth 2.0 上增加了标准化身份层:
openid scope 明确表示“这是一次 OIDC 认证请求”;ID Token 传递身份声明;最简洁的对比是:
| 对比项 | OAuth 2.0 | OIDC |
|---|---|---|
| 主要目标 | 授权:允许客户端访问资源 | 认证:确认用户身份,并可同时授权 |
| 核心问题 | “应用可以调用哪些 API?” | “用户是谁?” |
| 主要凭证 | Access Token | ID Token + Access Token |
| Token 面向对象 | Resource Server | ID Token 面向 Client,Access Token 面向 API |
| 用户信息 | 没有统一标准 | Claims、UserInfo 标准化 |
| 是否必须 JWT | OAuth 不要求 | ID Token 必须是 JWT 格式 |
| 典型 scope | read:orders | openid profile email |
| 验证重点 | Token 是否能访问 API | iss、sub、aud、exp、签名、nonce 等 |
不要把“拿到一个 OAuth Access Token”直接等价为“完成了 OIDC 登录”。
如果授权请求没有包含 openid scope,通常就是普通 OAuth 授权;即使返回了 JWT,也不能只因为它长得像 JWT 就把它当成 ID Token。Token 的用途由协议上下文、签发方、受众和验证规则共同决定。
ID Token 是 OP 发给客户端的身份声明,常见格式是 JWT,由三部分组成:
base64url(header).base64url(payload).base64url(signature)
一个简化的 Payload 示例:
{
"iss": "https://id.example.com",
"sub": "00u123456789",
"aud": "web-client-123",
"exp": 1720003600,
"iat": 1720000000,
"nonce": "n-0S6_WzA2Mj",
"auth_time": 1720000000,
"name": "Alice",
"email": "[email protected]",
"email_verified": true
}
客户端不能只 Base64 解码 Payload 就信任用户身份。至少应验证:
iss:必须等于客户端配置的 issuer;aud:必须包含当前客户端的 client_id;exp:不能已经过期;iat / auth_time:检查时间合理性和认证新鲜度;nonce:必须与客户端发起请求时保存的 nonce 一致,防止重放;azp:在特定多受众场景下验证 authorized party。sub 才是稳定用户标识应用不应使用 email 作为用户主键。OIDC 中推荐使用:
issuer + sub
这是一个在指定 issuer 下稳定且不可变的用户标识。不同 issuer 即使 email 相同,也不应默认视为同一个用户。
生产环境最常用的是 Authorization Code Flow + PKCE。它适合 Web 应用、SPA 和移动端,是当前推荐的通用模式。
sequenceDiagram
autonumber
participant U as 用户
participant C as Client / RP
participant OP as OpenID Provider
participant API as Resource Server
U->>C: 访问需要登录的页面
C->>C: 生成 state、nonce、PKCE code_verifier
C->>OP: GET /authorize?response_type=code&scope=openid...
OP->>U: 展示登录、同意和 MFA 页面
U->>OP: 完成认证
OP-->>C: 302 redirect_uri?code=...&state=...
C->>C: 校验 state
C->>OP: POST /token(code + code_verifier)
OP-->>C: access_token + id_token + refresh_token
C->>C: 验证 ID Token 签名和 Claims
C->>API: Authorization: Bearer access_token
API-->>C: 返回受保护资源
state:绑定请求与回调,主要防御 CSRF;nonce:绑定认证请求与 ID Token,主要防止 ID Token 重放;code_verifier 证明换取 code 的客户端就是发起授权请求的客户端,降低授权码被截获后的风险。客户端可以从以下元数据地址开始:
GET /.well-known/openid-configuration HTTP/1.1
Host: id.example.com
典型响应:
{
"issuer": "https://id.example.com",
"authorization_endpoint": "https://id.example.com/oauth2/authorize",
"token_endpoint": "https://id.example.com/oauth2/token",
"userinfo_endpoint": "https://id.example.com/oauth2/userinfo",
"jwks_uri": "https://id.example.com/.well-known/jwks.json",
"response_types_supported": ["code"],
"subject_types_supported": ["public"],
"id_token_signing_alg_values_supported": ["RS256"],
"scopes_supported": ["openid", "profile", "email"]
}
客户端应校验返回的 issuer,并避免把未经验证的 Discovery 地址直接当成可信配置。
GET /oauth2/authorize?
client_id=web-client-123&
response_type=code&
redirect_uri=https%3A%2F%2Fapp.example.com%2Fcallback&
scope=openid%20profile%20email&
state=af0ifjsldkj&
nonce=n-0S6_WzA2Mj&
code_challenge=E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM&
code_challenge_method=S256 HTTP/1.1
Host: id.example.com
关键参数:
| 参数 | 作用 |
|---|---|
client_id | 标识客户端 |
response_type=code | 使用授权码模式 |
redirect_uri | 认证完成后的回调地址,必须与注册值匹配 |
scope=openid | 声明这是 OIDC 请求 |
state | 防 CSRF 的随机值 |
nonce | 绑定本次认证与 ID Token |
code_challenge | PKCE 的挑战值 |
code_challenge_method=S256 | 指定挑战算法 |
成功时,OP 通常重定向到:
HTTP/1.1 302 Found
Location: https://app.example.com/callback?
code=SplxlOBeZQQYbYS6WxSbIA&
state=af0ifjsldkj
客户端必须先比较回调中的 state 与本地保存值,再使用 code。授权码是短期、一次性的中间凭证,不是 Access Token,也不是 ID Token。
失败时可能返回:
error=access_denied
error_description=The+user+denied+the+request
state=af0ifjsldkj
即使是错误响应,也应验证 state 后再处理。
服务端 Web 应用常通过后端调用 Token Endpoint:
POST /oauth2/token HTTP/1.1
Host: id.example.com
Content-Type: application/x-www-form-urlencoded
Authorization: Basic base64(client_id:client_secret)
grant_type=authorization_code&
code=SplxlOBeZQQYbYS6WxSbIA&
redirect_uri=https%3A%2F%2Fapp.example.com%2Fcallback&
code_verifier=dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk
公共客户端(例如 SPA、移动 App)通常不能安全保存 client_secret,因此依赖 PKCE,而不是把 secret 当作主要安全边界。
{
"access_token": "eyJhbGciOiJSUzI1NiIs...",
"token_type": "Bearer",
"expires_in": 3600,
"id_token": "eyJhbGciOiJSUzI1NiIs...",
"refresh_token": "def50200...",
"scope": "openid profile email"
}
安全处理原则:
id_token 用于客户端建立登录会话,并按 OIDC 规则验证;access_token 只发给它对应的 API,不能拿来代替 ID Token;当客户端需要更多用户 Claims 时,可以调用 UserInfo Endpoint:
GET /oauth2/userinfo HTTP/1.1
Host: id.example.com
Authorization: Bearer eyJhbGciOiJSUzI1NiIs...
响应示例:
{
"sub": "00u123456789",
"name": "Alice",
"preferred_username": "alice",
"email": "[email protected]",
"email_verified": true
}
UserInfo 响应中的 sub 必须与 ID Token 的 sub 一致,否则客户端不能把它们视为同一用户。
假设有一个订单管理后台 orders.example.com,公司使用 login.company.com 作为统一身份中心。
client_id: orders-web
redirect_uri: https://orders.example.com/oidc/callback
post_logout_redirect_uri: https://orders.example.com/
allowed_scopes: openid profile email orders.read orders.write
/orders,应用发现没有本地会话;state、nonce 和 PKCE 参数;state,用授权码换取 Token;iss + sub 查找或创建本地用户;OIDC Token 是协议凭证,而应用会话是业务系统自己的登录状态。服务端可以将验证后的身份映射到本地用户、角色、租户和权限,再通过 HttpOnly、Secure、SameSite Cookie 建立会话。这样可以避免把长期 Token 暴露给浏览器业务代码。
多个内部系统统一跳转到一个 IdP,用户只需登录一次。应用通过 iss + sub 识别身份,并从 Claims 或企业目录映射部门、角色和租户。
“使用 Google / Microsoft / GitHub 登录”通常使用 OIDC 或带有 OIDC 能力的 OAuth 方案。应用不接触用户密码,只接收标准化身份声明。
使用 Authorization Code Flow + PKCE。客户端不保存 client secret,授权码通过 code verifier 保护。
每个企业租户可以绑定不同 IdP。应用通过 Discovery 动态获取端点,并严格校验 issuer,避免错误地接受来自其他租户的 Token。
登录由 OIDC 负责,API 权限由 OAuth scope、角色和策略系统负责。二者结合,可以把“用户身份”和“接口授权”分层管理。
state 和 nonce;iss、aud、exp、nonce;OIDC 并不是 OAuth 的替代品,而是 OAuth 之上的身份层:
flowchart TB
OIDC[OpenID Connect:认证用户是谁]
OAuth[OAuth 2.0:授权访问哪些资源]
HTTP[HTTP / TLS:传输与保密]
OIDC --> OAuth --> HTTP
ID[ID Token:给 Client 的身份声明] --> OIDC
AT[Access Token:给 API 的访问凭证] --> OAuth
记住三句话:
openid scope 表示请求 OIDC 身份认证;ID Token 给客户端确认用户身份,Access Token 给 API 授权;理解这三层关系,就能看懂大多数 SSO、社交登录、企业身份联邦和现代 Web 登录系统。
No comments yet. Be the first!