OAuth 认证协议

OAuth 2.0 授权流程与前端实现

问题

说一说 OAuth 协议,它是如何工作的?

解答

OAuth 是一个开放标准的授权协议,允许用户授权第三方应用访问其在另一个服务上的资源,而无需暴露密码。

OAuth 2.0 角色

  • Resource Owner:资源所有者,即用户
  • Client:第三方应用
  • Authorization Server:授权服务器,颁发 token
  • Resource Server:资源服务器,存放用户资源

授权码模式(最常用)

+--------+                               +---------------+
|        |---(1) 授权请求 --------------->|   用户代理    |
|        |                               |   (浏览器)    |
|        |<--(2) 授权码 ------------------|               |
|        |                               +---------------+
| Client |
|        |                               +---------------+
|        |---(3) 授权码换 Token --------->| Authorization |
|        |<--(4) Access Token -----------|    Server     |
|        |                               +---------------+
|        |
|        |                               +---------------+
|        |---(5) 携带 Token 请求资源 ---->|   Resource    |
|        |<--(6) 返回资源 ---------------|    Server     |
+--------+                               +---------------+

前端实现示例

// 1. 发起授权请求
function initiateOAuth() {
  const params = new URLSearchParams({
    client_id: 'your_client_id',
    redirect_uri: 'https://yourapp.com/callback',
    response_type: 'code',
    scope: 'read:user',
    state: generateRandomState() // 防止 CSRF
  });
  
  // 跳转到授权页面
  window.location.href = `https://auth.example.com/authorize?${params}`;
}

// 2. 回调页面处理授权码
function handleCallback() {
  const params = new URLSearchParams(window.location.search);
  const code = params.get('code');
  const state = params.get('state');
  
  // 验证 state 防止 CSRF
  if (state !== localStorage.getItem('oauth_state')) {
    throw new Error('State mismatch');
  }
  
  // 用授权码换取 token(通常由后端完成)
  return exchangeCodeForToken(code);
}

// 3. 授权码换 Token(后端实现更安全)
async function exchangeCodeForToken(code) {
  const response = 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,
      client_id: 'your_client_id',
      client_secret: 'your_client_secret', // 应在后端处理
      redirect_uri: 'https://yourapp.com/callback'
    })
  });
  
  return response.json();
  // 返回: { access_token, refresh_token, expires_in, token_type }
}

// 4. 使用 Token 请求资源
async function fetchUserInfo(accessToken) {
  const response = await fetch('https://api.example.com/user', {
    headers: {
      'Authorization': `Bearer ${accessToken}`
    }
  });
  
  return response.json();
}

四种授权模式

模式适用场景安全性
授权码模式有后端的 Web 应用最高
隐式模式纯前端 SPA(已不推荐)较低
密码模式高度信任的应用中等
客户端凭证模式服务端对服务端

PKCE 扩展(SPA 推荐)

// 生成 code_verifier 和 code_challenge
async function generatePKCE() {
  // 生成随机字符串作为 code_verifier
  const verifier = generateRandomString(64);
  
  // 计算 code_challenge = BASE64URL(SHA256(code_verifier))
  const encoder = new TextEncoder();
  const data = encoder.encode(verifier);
  const hash = await crypto.subtle.digest('SHA-256', data);
  const challenge = base64UrlEncode(hash);
  
  return { verifier, challenge };
}

// 授权请求时带上 code_challenge
function initiateOAuthWithPKCE(codeChallenge) {
  const params = new URLSearchParams({
    client_id: 'your_client_id',
    redirect_uri: 'https://yourapp.com/callback',
    response_type: 'code',
    scope: 'read:user',
    state: generateRandomState(),
    code_challenge: codeChallenge,
    code_challenge_method: 'S256'
  });
  
  window.location.href = `https://auth.example.com/authorize?${params}`;
}

// 换取 token 时带上 code_verifier
async function exchangeWithPKCE(code, codeVerifier) {
  const response = 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,
      client_id: 'your_client_id',
      redirect_uri: 'https://yourapp.com/callback',
      code_verifier: codeVerifier // 无需 client_secret
    })
  });
  
  return response.json();
}

关键点

  • OAuth 是授权协议,不是认证协议;OpenID Connect 基于 OAuth 实现认证
  • 授权码模式最安全,token 交换在后端完成,避免暴露 client_secret
  • state 参数用于防止 CSRF 攻击,必须验证
  • SPA 应用使用 PKCE 扩展,无需 client_secret 也能安全授权
  • Access Token 有效期短,Refresh Token 用于续期