Skip to content
返回构建教程

OAuth 2.0 流程

实现带 PKCE 的授权码流程,安全访问第三方 API。

为什么

OAuth 是你的应用请求权限代表用户在另一服务行动的方式(读他们的 GitHub 仓库、发 Slack 消息)。带 PKCE 的授权码流程是 web 和移动应用唯一安全流程——隐式流程已弃用因在 URL 片段泄漏 token。PKCE(代码交换证明密钥)防被截获的授权码被攻击者交换,即使在公开客户端。搞错意味着要么用户连不上账户,要么更糟,攻击者劫持 OAuth 流程获其在第三方服务的数据访问。

何时使用

应用需代表用户调第三方 API 时。用库约 2-4 小时。仅需 SSO 可跳过(直接用认证提供方)。

如何做

1. 在提供方注册应用获 client_id + client_secret
   - 为每个环境配重定向 URI(无通配符)
   - secret 存 env,永不进客户端 bundle

2. 生成 PKCE code_verifier(43-128 随机字符)和 code_challenge
   - code_challenge = base64url(sha256(code_verifier))——S256 方法
   - verifier 存会话以便令牌交换时可用

3. 重定向用户到提供方 authorize 端点
   - 含:response_type=code、client_id、redirect_uri、scope、state、code_challenge、code_challenge_method=S256
   - state 是返回时验证的随机串(防 CSRF)

4. 处理重定向回来:用 code + verifier 换 access_token + refresh_token
   - POST 到 token 端点带 grant_type=authorization_code
   - 验证 state 匹配所发;不匹配则中止

5. 用 access_token 调 API;过期时刷新
   - 令牌加密存 DB(web 永不存 localStorage)
   - 过期 80% 时主动刷新避免请求中 401

示例

import { randomBytes, createHash } from "crypto";

// 步骤 2:PKCE
const verifier = randomBytes(32).toString("base64url");
const challenge = createHash("sha256").update(verifier).digest("base64url");
session.set("pkce_verifier", verifier);

// 步骤 3:重定向到 authorize
const authUrl = new URL("https://github.com/login/oauth/authorize");
authUrl.searchParams.set("client_id", env.GH_CLIENT_ID);
authUrl.searchParams.set("redirect_uri", `${env.URL}/api/oauth/callback`);
authUrl.searchParams.set("scope", "repo read:user");
authUrl.searchParams.set("state", session.get("oauth_state"));
authUrl.searchParams.set("code_challenge", challenge);
authUrl.searchParams.set("code_challenge_method", "S256");
redirect(authUrl.toString());

// 步骤 4:用 code 换 token
const tokenRes = await fetch("https://github.com/login/oauth/access_token", {
  method: "POST", headers: { "Accept": "application/json", "Content-Type": "application/json" },
  body: JSON.stringify({ client_id, client_secret, code, code_verifier: verifier, redirect_uri }),
});

常见问题

  • ⚠用隐式流程(response_type=token)因步骤少——它在 URL 栏泄漏 token。
  • ⚠不验证 state 参数,攻击者可发起流程并注入自己的 code。
  • ⚠access token 存 localStorage,任何 XSS 脚本可读——用 httpOnly cookie 或服务端会话。
  • ⚠忘刷新 token,集成在 1 小时后 access token 过期时坏。

最佳实践

  • ✓总用 PKCE,即使服务端应用——5 行加法防 code 截获攻击。
  • ✓请求最小所需 scope,让用户看到确切授予的权限。
  • ✓优雅处理'用户拒绝同意'——不报错,显示页解释无连接不能做什么。
  • ✓用库(lucia-auth、oslo 或提供方 SDK)——手写 OAuth 太多坑难做对。