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