跳到主要内容

OpenID Connect 和 OAuth 2.0

本文将讲解 Web 应用程序中 ID 管理和授权的标准协议。

什么是 OAuth 2.0

OAuth 2.0 是用于 授权 (Authorization) 的开放标准协议。

主要目的

  • 允许用户授予第三方应用程序访问其资源(数据)的权限
  • 在不共享密码的情况下,委派对特定资源的访问权限

OAuth 2.0 的四个主要角色

  1. 资源所有者 (Resource Owner): 资源的拥有者(通常是最终用户)
  2. 客户端 (Client): 想要访问资源的应用程序
  3. 授权服务器 (Authorization Server): 颁发访问令牌的服务器
  4. 资源服务器 (Resource Server): 提供受保护资源的服务器

OAuth 2.0 的主要流程

授权码模式 (Authorization Code Flow)

这是最安全且通常被推荐的流程。

特点:

  • 不直接将机密信息(访问令牌)传递给前端
  • 在后端进行令牌交换,因此安全性高
  • SPA 和原生应用也可以结合 PKCE 扩展使用

授权码模式 + PKCE (Authorization Code Flow with PKCE)

PKCE (Proof Key for Code Exchange) 是 OAuth 2.0 的扩展规范,用于防止授权码拦截攻击的安全增强方法。

为什么需要 PKCE:

  • SPA 和移动应用无法安全地存储 client_secret
  • 授权码可能会被拦截
  • 增强公共客户端(没有客户端密钥的客户端)的安全性

PKCE 的机制:

PKCE 的参数:

  1. code_verifier:

    • 43〜128 个字符的随机字符串
    • 客户端在本地生成并保存
    • 例如: dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk
  2. code_challenge:

    • code_verifier 的 SHA256 哈希值的 Base64URL 编码
    • 在授权请求时发送
    • 例如: E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM
  3. code_challenge_method:

    • S256 (推荐 SHA256) 或 plain (不推荐)

实现示例:

// 1. 生成 code_verifier
function generateCodeVerifier() {
const array = new Uint8Array(32);
crypto.getRandomValues(array);
return base64URLEncode(array);
}

// 2. 生成 code_challenge
async function generateCodeChallenge(verifier) {
const encoder = new TextEncoder();
const data = encoder.encode(verifier);
const hash = await crypto.subtle.digest('SHA-256', data);
return base64URLEncode(new Uint8Array(hash));
}

// Base64URL 编码
function base64URLEncode(buffer) {
return btoa(String.fromCharCode(...buffer))
.replace(/\+/g, '-')
.replace(/\//g, '_')
.replace(/=/g, '');
}

// 3. 授权请求
async function startAuthFlow() {
const codeVerifier = generateCodeVerifier();
const codeChallenge = await generateCodeChallenge(codeVerifier);

// 将 code_verifier 保存到会话存储中
sessionStorage.setItem('code_verifier', codeVerifier);

const authUrl = new URL('https://auth.example.com/authorize');
authUrl.searchParams.set('response_type', 'code');
authUrl.searchParams.set('client_id', 'your-client-id');
authUrl.searchParams.set('redirect_uri', 'https://yourapp.com/callback');
authUrl.searchParams.set('scope', 'openid profile email');
authUrl.searchParams.set('code_challenge', codeChallenge);
authUrl.searchParams.set('code_challenge_method', 'S256');
authUrl.searchParams.set('state', generateRandomState());

window.location.href = authUrl.toString();
}

// 4. 回调处理 (获取令牌)
async function handleCallback() {
const params = new URLSearchParams(window.location.search);
const code = params.get('code');
const state = params.get('state');

// 验证 state
if (state !== sessionStorage.getItem('state')) {
throw new Error('Invalid state');
}

// 获取保存的 code_verifier
const codeVerifier = sessionStorage.getItem('code_verifier');

// 请求令牌端点
const tokenResponse = await fetch('https://auth.example.com/token', {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
},
body: new URLSearchParams({
grant_type: 'authorization_code',
code: code,
redirect_uri: 'https://yourapp.com/callback',
client_id: 'your-client-id',
code_verifier: codeVerifier, // PKCE 的关键参数
}),
});

const tokens = await tokenResponse.json();

// 清理 code_verifier
sessionStorage.removeItem('code_verifier');
sessionStorage.removeItem('state');

return tokens;
}

PKCE 的优点:

  • ✅ 不需要 client_secret (最适合 SPA 和移动应用)
  • ✅ 防止授权码拦截攻击
  • ✅ 即使攻击者拦截了代码,如果没有 code_verifier 也无法获取令牌
  • ✅ 所有 OAuth 2.0 客户端推荐的标准

安全要点:

  • code_verifier 必须足够随机且不可预测
  • code_verifier 仅在客户端保存,不发送(仅在令牌交换时)
  • code_challenge 在授权服务器端安全保存
  • code_challenge_method 使用 S256 (plain 不推荐)

隐式流程 (Implicit Flow)

※ 目前不推荐。SPA 应使用授权码模式 + PKCE。

客户端凭证模式 (Client Credentials Flow)

用于服务器间通信的流程。

客户端 → 授权服务器: client_id + client_secret
授权服务器 → 客户端: 访问令牌
客户端 → 资源服务器: 使用访问令牌访问 API

用途:

  • 微服务间通信
  • 批处理
  • 不需要用户上下文的 API 调用

什么是 OpenID Connect (OIDC)

OpenID Connect 是构建在 OAuth 2.0 之上的 认证 (Authentication) 层。

与 OAuth 2.0 的区别

项目OAuth 2.0OpenID Connect
主要目的授权 (Authorization)认证 (Authentication)
获取的信息访问权限用户 ID 信息
令牌Access TokenID Token + Access Token
用例API 访问权限委派登录・SSO

ID Token

OpenID Connect 的核心概念是 ID Token

  • JWT (JSON Web Token) 格式
  • 包含用户的认证信息
  • 已签名,可验证是否被篡改

ID Token 结构示例:

{
"iss": "https://auth.example.com",
"sub": "user123",
"aud": "client-app-id",
"exp": 1735689600,
"iat": 1735686000,
"name": "山田太郎",
"email": "yamada@example.com",
"email_verified": true
}

主要声明 (Claims):

  • iss: 令牌颁发者
  • sub: 用户唯一标识符
  • aud: 令牌的目标客户端
  • exp: 有效期
  • iat: 签发时间

OpenID Connect 流程

基本上与 OAuth 2.0 的授权码模式相同,但有以下区别:

  1. Scope 中包含 openid

    scope=openid profile email
  2. 颁发 ID Token

    • 除了 Access Token 外,还获取 ID Token
    • ID Token 中包含用户信息
  3. UserInfo 端点

    • 可获取更详细的用户信息
    GET /userinfo
    Authorization: Bearer {access_token}

实现示例

客户端的授权请求

// 重定向到授权端点
const authUrl = new URL('https://auth.example.com/authorize');
authUrl.searchParams.set('response_type', 'code');
authUrl.searchParams.set('client_id', 'your-client-id');
authUrl.searchParams.set('redirect_uri', 'https://yourapp.com/callback');
authUrl.searchParams.set('scope', 'openid profile email');
authUrl.searchParams.set('state', generateRandomState()); // CSRF 对策

window.location.href = authUrl.toString();

后端获取令牌

// 在回调中接收授权码
app.get('/callback', async (req, res) => {
const { code, state } = req.query;

// 验证 state (CSRF 对策)
if (state !== req.session.state) {
return res.status(400).send('Invalid state');
}

// 请求令牌端点
const tokenResponse = await fetch('https://auth.example.com/token', {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
},
body: new URLSearchParams({
grant_type: 'authorization_code',
code: code,
redirect_uri: 'https://yourapp.com/callback',
client_id: 'your-client-id',
client_secret: 'your-client-secret',
}),
});

const tokens = await tokenResponse.json();
// tokens.access_token, tokens.id_token, tokens.refresh_token

// 验证 ID Token
const idToken = verifyJWT(tokens.id_token);

// 保存到会话
req.session.userId = idToken.sub;
req.session.accessToken = tokens.access_token;

res.redirect('/dashboard');
});

安全注意事项

必须的措施

  1. 使用 HTTPS

    • 所有通信必须通过 HTTPS 进行
  2. State 参数

    • 使用 state 参数进行验证以防止 CSRF 攻击
  3. PKCE (Proof Key for Code Exchange)

    • SPA 和移动应用必须使用
    • 防止授权码拦截攻击
  4. 安全存储令牌

    • 妥善保护 Access Token
    • 不要保存在浏览器的 LocalStorage 中 (XSS 对策)
  5. 最小化 Scope

    • 仅请求必要的最小 Scope
  6. 令牌有效期

    • 为 Access Token 设置较短的有效期
    • 实现使用 Refresh Token 更新的机制

主要提供商

  • Azure Active Directory (Microsoft Entra ID)
  • Auth0
  • Okta
  • Google Identity Platform
  • Amazon Cognito
  • Keycloak (开源)

总结

  • OAuth 2.0: API 访问授权框架
  • OpenID Connect: 基于 OAuth 2.0 的认证协议
  • 授权码模式: 最安全且推荐的流程
  • ID Token: 包含用户认证信息的 JWT
  • 安全: 必须采取 HTTPS、State、PKCE 等措施

参考链接