授权流程详解

本文档详细说明 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. 登录成功,获取用户信息                             │                       │
     │ <─────────────────────────────────────────────────────│                       │
     │                                                       │                       │
     ▼                                                       ▼                       ▼

第一步:构建授权请求

基本授权URL

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_id=client_abc123def456ghi789jkl012mno345pqr

redirect_uri

授权成功后的回调地址,必须满足:

  1. 完全匹配:与注册时填写的回调URL完全一致
  2. HTTPS优先:生产环境必须使用HTTPS
  3. 不能有查询参数:回调URL不应包含查询字符串
正确示例:
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/ (末尾斜杠会导致不匹配)

scope

作用域定义了应用请求的权限范围:

作用域 说明 用户信息返回字段
openid OpenID Connect基本身份 sub
profile 用户基本资料 name, preferred_username
email 用户邮箱信息 email, email_verified
read 读取用户数据权限 -
write 写入用户数据权限 -
scope=openid%20profile%20email

state

用于防止CSRF攻击的随机字符串:

生成要求:

  • 长度:至少32个字符
  • 随机性:使用加密安全的随机数生成器
  • 一次性:每次授权请求使用新的state值
// 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));
}

第二步:用户授权

授权页面展示

用户被重定向到授权页面后,系统会:

  1. 检查登录状态

    • 已登录:直接显示授权确认页面
    • 未登录:显示登录页面,登录后继续授权流程
  2. 显示授权确认页面

    • 显示应用名称
    • 显示请求的权限范围
    • 提供"同意"和"拒绝"按钮

授权确认页面示例

┌─────────────────────────────────────────────────────────────┐
│                                                             │
│    授权请求                                                 │
│                                                             │
│    "我的测试应用" 请求访问您的账户                           │
│                                                             │
│    该应用将获得以下权限:                                    │
│    ✓ 获取您的基本信息(用户名、昵称)                        │
│    ✓ 获取您的邮箱地址                                       │
│                                                             │
│    ┌─────────────┐    ┌─────────────┐                       │
│    │   同意授权   │    │   拒绝授权   │                       │
│    └─────────────┘    └─────────────┘                       │
│                                                             │
│    授权后将跳转到: example.com                              │
│                                                             │
└─────────────────────────────────────────────────────────────┘

用户操作

用户可以选择:

  1. 同意授权

    • 系统生成授权码
    • 重定向到回调地址
  2. 拒绝授权

    • 重定向到回调地址,附带错误信息
    • 错误代码:access_denied
  3. 取消操作

    • 关闭页面,不会重定向

第三步:处理授权回调

成功回调

用户同意授权后,系统重定向到您的回调地址:

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

公开客户端请求示例(使用PKCE)

公开客户端(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扩展详解

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

为什么需要PKCE?

在传统授权码流程中,攻击者可能:

  1. 拦截授权码
  2. 使用拦截的授权码换取令牌

PKCE通过在授权请求和令牌请求之间建立密码学绑定,防止这种攻击。

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. 验证通过后返回令牌                                          │
│                                                                 │
└─────────────────────────────────────────────────────────────────┘

完整PKCE实现

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随时获取新令牌。


最佳实践

1. 始终使用state参数

// 生成并保存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验证失败');
}

2. 公开客户端必须使用PKCE

// SPA、移动应用等公开客户端
const pkce = await PKCE.generate();
// 在授权请求中包含code_challenge
// 在令牌请求中包含code_verifier

3. 安全存储令牌

// 推荐:使用HttpOnly Cookie(需要服务器配合)
// 或使用Session Storage(页面关闭后清除)

// 不推荐:LocalStorage(易受XSS攻击)
// 不推荐:URL参数(会泄露)

4. 及时使用授权码

授权码有效期仅10分钟,收到后立即换取令牌:

// 在回调处理函数中立即换取令牌
async function handleCallback() {
    const code = getCodeFromUrl();
    const tokens = await exchangeCodeForToken(code); // 立即执行
}

5. 处理令牌过期

// 检查令牌是否即将过期
function isTokenExpiringSoon(expiresAt, threshold = 300) {
    return (expiresAt - Date.now() / 1000) < threshold;
}

// 主动刷新令牌
if (isTokenExpiringSoon(tokenExpiresAt)) {
    await refreshAccessToken();
}

下一步