本文档详细说明 user.blym.top OAuth 2.0授权服务的完整授权流程,帮助您深入理解每个步骤的实现细节。
OAuth 2.0授权码流程是最安全、最常用的授权方式,适用于服务器端Web应用。
┌─────────┐ ┌─────────────┐
│ │ │ │
│ 用户 │ │ 您的应用 │
│ │ │ │
└────┬────┘ └──────┬──────┘
│ │
│ 1. 用户点击"使用user.blym.top登录" │
│ ──────────────────────────────────────────────────────>│
│ │
│ 2. 生成state和PKCE验证码,构建授权URL │
│ <─────────────────────────────────────────────────────│
│ │
│ 3. 重定向到授权端点 │
│ ──────────────────────────────────────────────────────>│
│ │
│ ┌──────────────────────────────────┴──────────────────────────────┐
│ │ user.blym.top │
│ │ OAuth授权服务器 │
│ ├─────────────────────────────────────────────────────────────────┤
│ │ │
│ 4. 检查登录状态 │ │
│ ──────────────────>│ │
│ │ │
│ 5. 显示登录页面 │ (如未登录) │
│ <─────────────────│ │
│ │ │
│ 6. 用户登录 │ │
│ ──────────────────>│ │
│ │ │
│ 7. 显示授权确认页面 │ │
│ <─────────────────│ │
│ │ │
│ 8. 用户同意授权 │ │
│ ──────────────────>│ │
│ │ │
│ 9. 生成授权码 │ │
│ │ - 创建授权码记录 │
│ │ - 关联用户ID、客户端ID、作用域 │
│ │ - 设置过期时间(10分钟) │
│ │ - 保存PKCE验证码(如使用) │
│ │ │
│ 10. 重定向回回调 │ │
│ <─────────────────│ Location: redirect_uri?code=xxx&state=xxx │
│ │ │
│ └──────────────────────────────────┬──────────────────────────────┘
│ │
│ 11. 用户被重定向到回调地址 │
│ ──────────────────────────────────────────────────────>│
│ │
│ │ 12. 验证state参数
│ │
│ │ 13. 用授权码换取令牌
│ │ ─────────────────────>│
│ │ │
│ │ 14. 验证授权码 │ user.blym.top
│ │ - 检查是否过期 │
│ │ - 检查是否已使用 │
│ │ - 验证PKCE(如使用) │
│ │ │
│ │ 15. 生成令牌 │
│ │ - 访问令牌(1小时) │
│ │ - 刷新令牌(30天) │
│ │ │
│ │ 16. 返回令牌 │
│ │ <─────────────────────│
│ │ │
│ 17. 登录成功,获取用户信息 │ │
│ <─────────────────────────────────────────────────────│ │
│ │ │
▼ ▼ ▼
https://user.blym.top/oauth/authorize.php?{参数}
| 参数 | 必填 | 类型 | 说明 |
|---|---|---|---|
| client_id | 是 | string | 客户端ID,注册应用时获取 |
| redirect_uri | 是 | string | 回调地址,必须与注册时完全一致(包括协议、域名、端口、路径) |
| response_type | 是 | string | 固定值 code |
| scope | 否 | string | 请求的作用域,多个用空格分隔,默认 openid profile email |
| state | 强烈推荐 | string | 随机字符串,用于防止CSRF攻击,建议32位以上 |
| code_challenge | 否 | string | PKCE挑战码,公开客户端强烈推荐使用 |
| code_challenge_method | 否 | string | PKCE方法:S256(推荐)或 plain |
| nonce | 否 | string | OpenID Connect nonce值,用于防止重放攻击 |
客户端唯一标识符,在管理后台创建应用时自动生成。
client_id=client_abc123def456ghi789jkl012mno345pqr
授权成功后的回调地址,必须满足:
正确示例:
https://example.com/auth/callback
https://example.com/oauth/user.blym.top/callback
错误示例:
http://example.com/callback (生产环境不应使用HTTP)
https://example.com/callback?foo=bar (不应有查询参数)
https://example.com/callback/ (末尾斜杠会导致不匹配)
作用域定义了应用请求的权限范围:
| 作用域 | 说明 | 用户信息返回字段 |
|---|---|---|
| openid | OpenID Connect基本身份 | sub |
| profile | 用户基本资料 | name, preferred_username |
| 用户邮箱信息 | email, email_verified | |
| read | 读取用户数据权限 | - |
| write | 写入用户数据权限 | - |
scope=openid%20profile%20email
用于防止CSRF攻击的随机字符串:
生成要求:
// JavaScript生成state
function generateState() {
const array = new Uint8Array(32);
crypto.getRandomValues(array);
return Array.from(array, byte => byte.toString(16).padStart(2, '0')).join('');
}
// PHP生成state
function generateState() {
return bin2hex(random_bytes(32));
}
用户被重定向到授权页面后,系统会:
检查登录状态
显示授权确认页面
┌─────────────────────────────────────────────────────────────┐
│ │
│ 授权请求 │
│ │
│ "我的测试应用" 请求访问您的账户 │
│ │
│ 该应用将获得以下权限: │
│ ✓ 获取您的基本信息(用户名、昵称) │
│ ✓ 获取您的邮箱地址 │
│ │
│ ┌─────────────┐ ┌─────────────┐ │
│ │ 同意授权 │ │ 拒绝授权 │ │
│ └─────────────┘ └─────────────┘ │
│ │
│ 授权后将跳转到: example.com │
│ │
└─────────────────────────────────────────────────────────────┘
用户可以选择:
同意授权
拒绝授权
access_denied取消操作
用户同意授权后,系统重定向到您的回调地址:
https://example.com/callback?code=AUTHORIZATION_CODE&state=ORIGINAL_STATE
参数说明:
| 参数 | 说明 |
|---|---|
| code | 授权码,用于换取访问令牌,有效期10分钟,只能使用一次 |
| state | 原始state值,需验证是否与发送时一致 |
如果授权过程中出现错误,系统重定向到回调地址并附带错误信息:
https://example.com/callback?error=ERROR_CODE&error_description=ERROR_DESCRIPTION&state=ORIGINAL_STATE
错误代码:
| 错误代码 | 说明 | 处理建议 |
|---|---|---|
| invalid_request | 请求缺少必要参数 | 检查请求参数 |
| unauthorized_client | 客户端未授权使用此授权类型 | 检查客户端配置 |
| access_denied | 用户拒绝授权 | 提示用户授权被拒绝 |
| unsupported_response_type | 不支持的response_type | 确保使用 code |
| invalid_scope | 请求的作用域无效 | 检查scope参数 |
| server_error | 服务器内部错误 | 联系管理员 |
// JavaScript处理回调
async function handleCallback() {
const urlParams = new URLSearchParams(window.location.search);
// 检查是否有错误
const error = urlParams.get('error');
if (error) {
const errorDesc = urlParams.get('error_description');
console.error('授权失败:', error, errorDesc);
showError(errorDesc || error);
return;
}
// 验证state
const returnedState = urlParams.get('state');
const savedState = sessionStorage.getItem('oauth_state');
if (!returnedState || returnedState !== savedState) {
console.error('State验证失败,可能存在CSRF攻击');
showError('安全验证失败,请重试');
return;
}
// 清除已使用的state
sessionStorage.removeItem('oauth_state');
// 获取授权码
const code = urlParams.get('code');
if (!code) {
console.error('未收到授权码');
showError('授权码缺失');
return;
}
// 用授权码换取令牌
try {
const tokens = await exchangeCodeForToken(code);
saveTokens(tokens);
redirectToApp();
} catch (err) {
console.error('获取令牌失败:', err);
showError('登录失败,请重试');
}
}
<?php
// PHP处理回调
function handleCallback() {
// 检查错误
if (isset($_GET['error'])) {
$error = $_GET['error'];
$errorDesc = $_GET['error_description'] ?? $error;
throw new Exception("授权失败: $errorDesc");
}
// 验证state
$returnedState = $_GET['state'] ?? '';
$savedState = $_SESSION['oauth_state'] ?? '';
if (empty($returnedState) || $returnedState !== $savedState) {
throw new Exception('State验证失败,可能存在CSRF攻击');
}
// 清除已使用的state
unset($_SESSION['oauth_state']);
// 获取授权码
$code = $_GET['code'] ?? '';
if (empty($code)) {
throw new Exception('授权码缺失');
}
// 用授权码换取令牌
$tokens = exchangeCodeForToken($code);
saveTokens($tokens);
return $tokens;
}
?>
POST https://user.blym.top/oauth/token.php
Content-Type: application/x-www-form-urlencoded
| 参数 | 必填 | 说明 |
|---|---|---|
| grant_type | 是 | 固定值 authorization_code |
| code | 是 | 授权码 |
| redirect_uri | 是 | 回调地址,必须与授权请求时一致 |
| client_id | 是 | 客户端ID |
| client_secret | 条件 | 客户端密钥(机密客户端必填,公开客户端使用PKCE则不需要) |
| code_verifier | 条件 | PKCE验证码(使用PKCE时必填) |
机密客户端(服务器端应用)使用client_secret认证:
POST /oauth/token.php HTTP/1.1
Host: user.blym.top
Content-Type: application/x-www-form-urlencoded
grant_type=authorization_code
&code=a1b2c3d4e5f6g7h8i9j0
&redirect_uri=https%3A%2F%2Fexample.com%2Fcallback
&client_id=client_abc123
&client_secret=secret_xyz789
公开客户端(SPA、移动应用)使用PKCE:
POST /oauth/token.php HTTP/1.1
Host: user.blym.top
Content-Type: application/x-www-form-urlencoded
grant_type=authorization_code
&code=a1b2c3d4e5f6g7h8i9j0
&redirect_uri=https%3A%2F%2Fexample.com%2Fcallback
&client_id=client_abc123
&code_verifier=dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk
HTTP/1.1 200 OK
Content-Type: application/json
Cache-Control: no-store
Pragma: no-cache
{
"access_token": "at_a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6",
"token_type": "Bearer",
"expires_in": 3600,
"refresh_token": "rt_q1r2s3t4u5v6w7x8y9z0a1b2c3d4e5f6",
"scope": "openid profile email",
"id_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJodHRwczovL3VzZXIuYmx5bS50b3AiLCJzdWIiOiIxMjMiLCJhdWQiOiJjbGllbnRfYWJjMTIzIiwiaWF0IjoxNzAwMDAwMDAwLCJleHAiOjE3MDAwMDM2MDB9.signature"
}
响应字段说明:
| 字段 | 类型 | 说明 |
|---|---|---|
| access_token | string | 访问令牌,用于调用受保护API |
| token_type | string | 令牌类型,固定为 Bearer |
| expires_in | number | 访问令牌有效期(秒),默认3600秒(1小时) |
| refresh_token | string | 刷新令牌,用于获取新的访问令牌 |
| scope | string | 实际授予的作用域 |
| id_token | string | OpenID Connect ID令牌(仅当scope包含openid时返回) |
HTTP/1.1 400 Bad Request
Content-Type: application/json
Cache-Control: no-store
Pragma: no-cache
{
"error": "invalid_grant",
"error_description": "Authorization code has expired"
}
错误代码:
| 错误代码 | HTTP状态码 | 说明 | 处理建议 |
|---|---|---|---|
| invalid_request | 400 | 请求缺少必要参数 | 检查请求参数 |
| invalid_client | 401 | 客户端认证失败 | 检查client_id和client_secret |
| invalid_grant | 400 | 授权码无效、过期或已使用 | 重新发起授权请求 |
| unauthorized_client | 400 | 客户端未授权 | 检查客户端配置 |
| unsupported_grant_type | 400 | 不支持的授权类型 | 确保使用 authorization_code |
| invalid_scope | 400 | 请求的作用域无效 | 检查scope参数 |
PKCE (Proof Key for Code Exchange) 是OAuth 2.0的安全扩展,用于防止授权码拦截攻击。
在传统授权码流程中,攻击者可能:
PKCE通过在授权请求和令牌请求之间建立密码学绑定,防止这种攻击。
┌─────────────────────────────────────────────────────────────────┐
│ PKCE流程 │
├─────────────────────────────────────────────────────────────────┤
│ │
│ 1. 客户端生成 code_verifier │
│ - 随机字符串,43-128个字符 │
│ - 使用 [A-Z] [a-z] [0-9] - . _ ~ 字符集 │
│ │
│ 2. 客户端计算 code_challenge │
│ - code_challenge = BASE64URL(SHA256(code_verifier)) │
│ - 或 code_challenge = code_verifier (plain方法,不推荐) │
│ │
│ 3. 授权请求携带 code_challenge │
│ - 服务器保存 code_challenge │
│ │
│ 4. 令牌请求携带 code_verifier │
│ - 服务器验证: │
│ BASE64URL(SHA256(code_verifier)) == code_challenge │
│ │
│ 5. 验证通过后返回令牌 │
│ │
└─────────────────────────────────────────────────────────────────┘
class PKCE {
// 生成code_verifier
static generateVerifier() {
const array = new Uint8Array(32);
crypto.getRandomValues(array);
return this.base64UrlEncode(array);
}
// 生成code_challenge
static async generateChallenge(verifier) {
const encoder = new TextEncoder();
const data = encoder.encode(verifier);
const digest = await crypto.subtle.digest('SHA-256', data);
return this.base64UrlEncode(new Uint8Array(digest));
}
// Base64 URL编码
static base64UrlEncode(array) {
let str = '';
array.forEach(byte => {
str += String.fromCharCode(byte);
});
return btoa(str)
.replace(/\+/g, '-')
.replace(/\//g, '_')
.replace(/=+$/, '');
}
// 完整流程
static async generate() {
const verifier = this.generateVerifier();
const challenge = await this.generateChallenge(verifier);
return { verifier, challenge };
}
}
// 使用示例
async function startAuthFlow() {
// 生成PKCE参数
const pkce = await PKCE.generate();
// 保存verifier用于令牌请求
sessionStorage.setItem('code_verifier', pkce.verifier);
// 生成state
const state = crypto.randomUUID();
sessionStorage.setItem('oauth_state', state);
// 构建授权URL
const params = new URLSearchParams({
client_id: 'your_client_id',
redirect_uri: 'https://example.com/callback',
response_type: 'code',
scope: 'openid profile email',
state: state,
code_challenge: pkce.challenge,
code_challenge_method: 'S256'
});
// 跳转到授权页面
window.location.href = `https://user.blym.top/oauth/authorize.php?${params}`;
}
// 处理回调
async function handleCallback() {
const urlParams = new URLSearchParams(window.location.search);
const code = urlParams.get('code');
const verifier = sessionStorage.getItem('code_verifier');
// 用授权码和verifier换取令牌
const response = await fetch('https://user.blym.top/oauth/token.php', {
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
body: new URLSearchParams({
grant_type: 'authorization_code',
code: code,
redirect_uri: 'https://example.com/callback',
client_id: 'your_client_id',
code_verifier: verifier
})
});
const tokens = await response.json();
return tokens;
}
客户端凭证模式适用于机器对机器通信,不需要用户参与。
POST /oauth/token.php HTTP/1.1
Host: user.blym.top
Content-Type: application/x-www-form-urlencoded
grant_type=client_credentials
&client_id=client_abc123
&client_secret=secret_xyz789
&scope=read write
{
"access_token": "at_machine_token_here",
"token_type": "Bearer",
"expires_in": 3600,
"scope": "read write"
}
注意: 客户端凭证模式不返回refresh_token,因为客户端可以使用client_credentials随时获取新令牌。
// 生成并保存state
const state = crypto.randomUUID();
sessionStorage.setItem('oauth_state', state);
// 在回调中验证
const returnedState = urlParams.get('state');
if (returnedState !== sessionStorage.getItem('oauth_state')) {
throw new Error('State验证失败');
}
// SPA、移动应用等公开客户端
const pkce = await PKCE.generate();
// 在授权请求中包含code_challenge
// 在令牌请求中包含code_verifier
// 推荐:使用HttpOnly Cookie(需要服务器配合)
// 或使用Session Storage(页面关闭后清除)
// 不推荐:LocalStorage(易受XSS攻击)
// 不推荐:URL参数(会泄露)
授权码有效期仅10分钟,收到后立即换取令牌:
// 在回调处理函数中立即换取令牌
async function handleCallback() {
const code = getCodeFromUrl();
const tokens = await exchangeCodeForToken(code); // 立即执行
}
// 检查令牌是否即将过期
function isTokenExpiringSoon(expiresAt, threshold = 300) {
return (expiresAt - Date.now() / 1000) < threshold;
}
// 主动刷新令牌
if (isTokenExpiringSoon(tokenExpiresAt)) {
await refreshAccessToken();
}