OpenID Connect #
概述 #
OpenID Connect(OIDC)是建立在 OAuth 2.0 之上的身份认证协议。它在 OAuth 2.0 的授权功能基础上,添加了身份认证层,使客户端能够验证用户身份并获取基本的用户信息。
text
┌─────────────────────────────────────────────────────────────┐
│ OpenID Connect 定位 │
├─────────────────────────────────────────────────────────────┤
│ │
│ OAuth 2.0:授权协议 │
│ "这个应用能访问你的数据吗?" │
│ │
│ OpenID Connect:身份认证协议 │
│ "你是谁?" │
│ │
│ 关系: │
│ OIDC = OAuth 2.0 + ID Token + UserInfo Endpoint │
│ │
│ 核心功能: │
│ ✅ 身份认证 │
│ ✅ 用户信息获取 │
│ ✅ 单点登录(SSO) │
│ ✅ 身份联合 │
│ │
└─────────────────────────────────────────────────────────────┘
OAuth 2.0 vs OpenID Connect #
核心区别 #
text
┌─────────────────────────────────────────────────────────────┐
│ OAuth 2.0 vs OIDC 对比 │
├─────────────────────────────────────────────────────────────┤
│ │
│ OAuth 2.0: │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ 目的:授权 │ │
│ │ 输出:Access Token │ │
│ │ 用途:访问受保护资源 │ │
│ │ 问题:无法标准化获取用户身份 │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
│ OpenID Connect: │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ 目的:身份认证 + 授权 │ │
│ │ 输出:ID Token + Access Token │ │
│ │ 用途:验证身份 + 访问资源 │ │
│ │ 优势:标准化用户身份信息 │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────┘
OIDC 新增内容 #
text
┌─────────────────────────────────────────────────────────────┐
│ OIDC 新增内容 │
├─────────────────────────────────────────────────────────────┤
│ │
│ 1. ID Token │
│ - JWT 格式的身份令牌 │
│ - 包含用户身份信息 │
│ │
│ 2. UserInfo Endpoint │
│ - 获取详细用户信息的端点 │
│ │
│ 3. 标准化 Scope │
│ - openid(必需) │
│ - profile, email, address, phone │
│ │
│ 4. Discovery │
│ - 自动发现服务配置 │
│ │
│ 5. 动态注册 │
│ - 客户端动态注册 │
│ │
│ 6. Session Management │
│ - 会话管理 │
│ - 登出支持 │
│ │
└─────────────────────────────────────────────────────────────┘
ID Token #
ID Token 结构 #
ID Token 是一个 JWT,包含用户的身份信息:
json
{
"iss": "https://auth.example.com",
"sub": "user-123",
"aud": "client-app",
"exp": 1516239022,
"iat": 1516239022,
"auth_time": 1516239000,
"nonce": "n-0S6_WzA2Mj",
"acr": "urn:mace:incommon:iap:silver",
"amr": ["pwd", "mfa"],
"azp": "client-app",
"at_hash": "MTIzNDU2Nzg5MDEyMzQ1Ng",
"c_hash": "MTIzNDU2Nzg5MDEyMzQ1Ng"
}
ID Token 声明 #
| 声明 | 名称 | 描述 |
|---|---|---|
| iss | Issuer | 签发者标识 |
| sub | Subject | 用户唯一标识 |
| aud | Audience | 接收方(客户端 ID) |
| exp | Expiration | 过期时间 |
| iat | Issued At | 签发时间 |
| auth_time | Authentication Time | 用户认证时间 |
| nonce | Nonce | 防重放攻击的随机值 |
| acr | Authentication Context Class Reference | 认证上下文级别 |
| amr | Authentication Methods References | 使用的认证方法 |
| azp | Authorized Party | 授权方(客户端 ID) |
| at_hash | Access Token Hash | 访问令牌哈希 |
| c_hash | Code Hash | 授权码哈希 |
ID Token vs Access Token #
text
┌─────────────────────────────────────────────────────────────┐
│ ID Token vs Access Token │
├─────────────────────────────────────────────────────────────┤
│ │
│ ID Token: │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ 目的:身份认证 │ │
│ │ 格式:JWT │ │
│ │ 内容:用户身份信息 │ │
│ │ 使用:客户端验证用户身份 │ │
│ │ 验证:客户端验证签名 │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
│ Access Token: │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ 目的:访问资源 │ │
│ │ 格式:任意(通常是 JWT) │ │
│ │ 内容:授权信息 │ │
│ │ 使用:访问资源服务器 │ │
│ │ 验证:资源服务器验证 │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────┘
OIDC 授权流程 #
授权码流程 #
text
┌─────────────────────────────────────────────────────────────┐
│ OIDC 授权码流程 │
├─────────────────────────────────────────────────────────────┤
│ │
│ ┌─────────┐ ┌─────────┐ ┌─────────┐ │
│ │ 用户 │ │ 客户端 │ │ OIDC │ │
│ │ │ │ │ │ Provider│ │
│ └────┬────┘ └────┬────┘ └────┬────┘ │
│ │ │ │ │
│ │ 1. 登录请求 │ │ │
│ │──────────────>│ │ │
│ │ │ │ │
│ │ │ 2. 重定向到认证页面 │
│ │ │ scope=openid profile email │
│ │<──────────────────────────────│ │
│ │ │ │ │
│ │ 3. 用户认证并授权 │ │
│ │──────────────────────────────>│ │
│ │ │ │ │
│ │ │ 4. 返回授权码│ │
│ │ │<──────────────│ │
│ │ │ │ │
│ │ │ 5. 获取令牌 │ │
│ │ │──────────────>│ │
│ │ │ │ │
│ │ │ 6. 返回令牌 │ │
│ │ │ ID Token + │ │
│ │ │ Access Token │ │
│ │ │<──────────────│ │
│ │ │ │ │
│ │ │ 7. 验证 ID Token │
│ │ │ │ │
│ │ 8. 登录成功 │ │ │
│ │<──────────────│ │ │
│ │ │ │ │
└─────────────────────────────────────────────────────────────┘
授权请求 #
http
GET /authorize?
response_type=code
&client_id=your-client-id
&redirect_uri=https%3A%2F%2Fyour-app.example.com%2Fcallback
&scope=openid%20profile%20email
&state=abc123
&nonce=xyz789 HTTP/1.1
Host: auth.example.com
关键参数:
| 参数 | 描述 |
|---|---|
| scope | 必须包含 openid |
| nonce | 防重放攻击的随机值 |
令牌响应 #
json
{
"access_token": "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9...",
"id_token": "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9...",
"token_type": "Bearer",
"expires_in": 3600,
"refresh_token": "8xLOxBtZp8",
"scope": "openid profile email"
}
OIDC Scope #
标准 Scope #
text
┌─────────────────────────────────────────────────────────────┐
│ OIDC 标准 Scope │
├─────────────────────────────────────────────────────────────┤
│ │
│ openid(必需) │
│ - 表示这是一个 OIDC 请求 │
│ - 返回 ID Token │
│ - 包含 sub 声明 │
│ │
│ profile │
│ - name, family_name, given_name │
│ - middle_name, nickname │
│ - preferred_username, profile │
│ - picture, website, gender │
│ - birthdate, zoneinfo, locale │
│ - updated_at │
│ │
│ email │
│ - email, email_verified │
│ │
│ address │
│ - formatted, street_address │
│ - locality, region, postal_code, country │
│ │
│ phone │
│ - phone_number, phone_number_verified │
│ │
│ offline_access │
│ - 请求刷新令牌 │
│ │
└─────────────────────────────────────────────────────────────┘
UserInfo Endpoint #
获取用户信息 #
http
GET /userinfo HTTP/1.1
Host: auth.example.com
Authorization: Bearer eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9...
响应示例 #
json
{
"sub": "user-123",
"name": "John Doe",
"given_name": "John",
"family_name": "Doe",
"preferred_username": "johndoe",
"email": "john@example.com",
"email_verified": true,
"picture": "https://example.com/john.jpg",
"locale": "zh-CN",
"updated_at": 1516239022
}
实现示例 #
javascript
async function getUserInfo(accessToken) {
const response = await fetch('https://auth.example.com/userinfo', {
headers: {
'Authorization': `Bearer ${accessToken}`
}
});
return await response.json();
}
const userInfo = await getUserInfo(accessToken);
console.log(userInfo);
Discovery #
什么是 Discovery? #
text
┌─────────────────────────────────────────────────────────────┐
│ OIDC Discovery │
├─────────────────────────────────────────────────────────────┤
│ │
│ Discovery 允许客户端自动发现 OIDC 提供者的配置 │
│ │
│ 发现端点: │
│ https://auth.example.com/.well-known/openid-configuration │
│ │
│ 优势: │
│ ✅ 无需手动配置端点 │
│ ✅ 自动适应配置变化 │
│ ✅ 简化客户端实现 │
│ │
└─────────────────────────────────────────────────────────────┘
Discovery 响应 #
json
{
"issuer": "https://auth.example.com",
"authorization_endpoint": "https://auth.example.com/authorize",
"token_endpoint": "https://auth.example.com/token",
"userinfo_endpoint": "https://auth.example.com/userinfo",
"jwks_uri": "https://auth.example.com/.well-known/jwks.json",
"revocation_endpoint": "https://auth.example.com/revoke",
"end_session_endpoint": "https://auth.example.com/logout",
"scopes_supported": [
"openid", "profile", "email", "address", "phone", "offline_access"
],
"response_types_supported": ["code", "token", "id_token", "code id_token"],
"grant_types_supported": [
"authorization_code", "implicit", "refresh_token"
],
"subject_types_supported": ["public"],
"id_token_signing_alg_values_supported": ["RS256", "ES256"],
"claims_supported": [
"sub", "name", "email", "email_verified", "picture"
]
}
使用 Discovery #
javascript
async function discoverProvider(issuer) {
const response = await fetch(
`${issuer}/.well-known/openid-configuration`
);
return await response.json();
}
const config = await discoverProvider('https://auth.example.com');
console.log(config.authorization_endpoint);
console.log(config.token_endpoint);
ID Token 验证 #
验证步骤 #
text
┌─────────────────────────────────────────────────────────────┐
│ ID Token 验证步骤 │
├─────────────────────────────────────────────────────────────┤
│ │
│ 1. 解码 JWT │
│ - 分割 Header、Payload、Signature │
│ │
│ 2. 验证签名 │
│ - 使用 JWKS 公钥验证 │
│ │
│ 3. 验证 iss 声明 │
│ - 必须与 Discovery 中的 issuer 匹配 │
│ │
│ 4. 验证 aud 声明 │
│ - 必须包含自己的 client_id │
│ │
│ 5. 验证 exp 声明 │
│ - 当前时间必须小于过期时间 │
│ │
│ 6. 验证 iat 声明 │
│ - 签发时间在合理范围内 │
│ │
│ 7. 验证 nonce 声明 │
│ - 必须与请求时发送的 nonce 匹配 │
│ │
│ 8. 验证 at_hash(可选) │
│ - 如果存在,验证访问令牌哈希 │
│ │
└─────────────────────────────────────────────────────────────┘
验证实现 #
javascript
const jwt = require('jsonwebtoken');
const jwksClient = require('jwks-rsa');
async function verifyIdToken(idToken, clientId, issuer, nonce) {
const client = jwksClient({
jwksUri: `${issuer}/.well-known/jwks.json`
});
const decoded = await new Promise((resolve, reject) => {
jwt.verify(idToken, (header, callback) => {
client.getSigningKey(header.kid, (err, key) => {
if (err) {
callback(err);
} else {
callback(null, key.publicKey || key.rsaPublicKey);
}
});
}, {
issuer: issuer,
audience: clientId,
algorithms: ['RS256']
}, (err, decoded) => {
if (err) {
reject(err);
} else {
resolve(decoded);
}
});
});
if (nonce && decoded.nonce !== nonce) {
throw new Error('Nonce mismatch');
}
return decoded;
}
完整实现示例 #
Node.js + Express #
javascript
const express = require('express');
const session = require('express-session');
const crypto = require('crypto');
const axios = require('axios');
const app = express();
app.use(session({
secret: 'your-secret',
resave: false,
saveUninitialized: false
}));
const config = {
clientId: 'your-client-id',
clientSecret: 'your-client-secret',
redirectUri: 'http://localhost:3000/callback',
scope: 'openid profile email'
};
let oidcConfig = null;
async function getOidcConfig() {
if (!oidcConfig) {
const response = await axios.get(
'https://auth.example.com/.well-known/openid-configuration'
);
oidcConfig = response.data;
}
return oidcConfig;
}
app.get('/login', async (req, res) => {
const config = await getOidcConfig();
const state = crypto.randomBytes(16).toString('hex');
const nonce = crypto.randomBytes(16).toString('hex');
req.session.state = state;
req.session.nonce = nonce;
const params = new URLSearchParams({
response_type: 'code',
client_id: config.clientId,
redirect_uri: config.redirectUri,
scope: config.scope,
state: state,
nonce: nonce
});
res.redirect(`${config.authorization_endpoint}?${params}`);
});
app.get('/callback', async (req, res) => {
const { code, state } = req.query;
if (state !== req.session.state) {
return res.status(400).send('Invalid state');
}
const oidcConfig = await getOidcConfig();
const tokenResponse = await axios.post(oidcConfig.token_endpoint, {
grant_type: 'authorization_code',
code: code,
redirect_uri: config.redirectUri,
client_id: config.clientId,
client_secret: config.clientSecret
});
const { id_token, access_token } = tokenResponse.data;
const userInfo = await axios.get(oidcConfig.userinfo_endpoint, {
headers: {
'Authorization': `Bearer ${access_token}`
}
});
req.session.user = userInfo.data;
res.redirect('/dashboard');
});
app.listen(3000);
单点登录(SSO) #
SSO 流程 #
text
┌─────────────────────────────────────────────────────────────┐
│ OIDC SSO 流程 │
├─────────────────────────────────────────────────────────────┤
│ │
│ 应用 A 登录: │
│ 1. 用户访问应用 A │
│ 2. 重定向到 OIDC Provider │
│ 3. 用户认证 │
│ 4. 返回应用 A,建立会话 │
│ │
│ 应用 B 登录(无需再次认证): │
│ 1. 用户访问应用 B │
│ 2. 重定向到 OIDC Provider │
│ 3. Provider 检测到已有会话 │
│ 4. 直接返回应用 B,建立会话 │
│ │
│ 关键: │
│ - OIDC Provider 维护用户会话 │
│ - 多个应用共享同一会话 │
│ - prompt=none 可实现静默登录 │
│ │
└─────────────────────────────────────────────────────────────┘
静默登录 #
http
GET /authorize?
response_type=code
&client_id=app-b
&redirect_uri=https%3A%2F%2Fapp-b.example.com%2Fcallback
&scope=openid%20profile
&prompt=none HTTP/1.1
Host: auth.example.com
登出 #
RP-Initiated Logout #
http
GET /logout?
id_token_hint=eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9...
&post_logout_redirect_uri=https%3A%2F%2Fyour-app.example.com
&state=abc123 HTTP/1.1
Host: auth.example.com
前端登出实现 #
javascript
async function logout(idToken) {
const oidcConfig = await getOidcConfig();
const params = new URLSearchParams({
id_token_hint: idToken,
post_logout_redirect_uri: window.location.origin,
state: crypto.randomUUID()
});
window.location.href = `${oidcConfig.end_session_endpoint}?${params}`;
}
OIDC 流程类型 #
Authorization Code Flow #
text
最安全、最完整的流程
适用于:Web 服务器应用、移动应用、SPA
步骤:
1. 获取授权码
2. 用授权码换取令牌
3. 验证 ID Token
Implicit Flow(已弃用) #
text
直接返回令牌,无授权码交换
适用于:纯前端应用(已不推荐)
问题:
- 令牌暴露在 URL 中
- 无法使用刷新令牌
- 已被 Authorization Code + PKCE 取代
Hybrid Flow #
text
结合授权码和隐式流程
适用于:需要在前端立即获取 ID Token 的场景
返回:
- 授权码
- 部分 ID Token(可选)
- 部分 Access Token(可选)
下一步 #
现在你已经了解了 OpenID Connect,接下来学习 安全最佳实践,了解 OAuth 和 OIDC 的安全实现要点!
最后更新:2026-03-28