OAuth 2.0 是目前最主流的授权框架,允许第三方应用在无需获取用户密码的前提下,安全地访问受保护的资源。

假设读者具备基础的 HTTP 和 Web 开发经验。

核心概念 Link to heading

四个角色 Link to heading

角色说明示例
Resource Owner资源所有者(用户)登录 Google 的张三
Client请求资源的第三方应用“用 Google 登录” 的博客网站
Authorization Server授权服务器,验证用户并发放 Tokenaccounts.google.com
Resource Server资源服务器,存放受保护资源Google Contacts API

授权流程概览 Link to heading

sequenceDiagram participant U as 用户 participant C as 第三方应用 participant AS as 授权服务器 participant RS as 资源服务器 U->>C: 1. 点击"授权登录" C->>AS: 2. 跳转至授权页 AS->>U: 3. 用户登录并确认授权 U->>C: 4. 返回授权码 (code) C->>AS: 5. 用 code 换取 access_token AS->>C: 6. 返回 access_token C->>RS: 7. 携带 token 请求资源 RS->>C: 8. 返回受保护资源

为什么不用密码直连? Link to heading

传统方式:第三方应用直接索要用户名和密码 → 密码泄露风险、无法精细化控制权限、用户无法单独撤销第三方访问。

OAuth 2.0 的方案:用户密码只存在于授权服务器,第三方应用只拿到受限的 access_token,可随时撤销。

四种授权模式 Link to heading

模式适用场景安全性
Authorization CodeWeb 服务端应用(推荐)最高
Implicit单页应用(SPA,已废弃)
Resource Owner Password Credentials高度信任的应用(已废弃)
Client Credentials机器对机器(M2M)

现代应用主要使用 Authorization Code + PKCE 模式,也是本文重点。

关键术语 Link to heading

  • access_token:访问令牌,调用 API 的"钥匙",有有效期
  • refresh_token:刷新令牌,用于获取新的 access_token
  • scope:权限范围,如 read:profilewrite:contacts
  • PKCE(Proof Key for Code Exchange):防止授权码被拦截的扩展机制,RFC 7636

Authorization Code + PKCE 完整流程 Link to heading

这是目前最安全、最通用的模式,适用于 Web 应用、移动端和单页应用。

第一步:生成 PKCE 参数 Link to heading

import secrets
import base64
import hashlib

def generate_pkce_params():
    """生成 code_verifier 和 code_challenge"""
    # 1. 生成随机 code_verifier(43-128 字符)
    code_verifier = secrets.token_urlsafe(64)

    # 2. 计算 code_challenge = base64url(SHA256(code_verifier))
    code_challenge = base64.urlsafe_b64encode(
        hashlib.sha256(code_verifier.encode()).digest()
    ).decode().rstrip('=')

    return code_verifier, code_challenge

verifier, challenge = generate_pkce_params()
print(f"code_verifier:   {verifier}")
print(f"code_challenge:  {challenge}")

第二步:引导用户跳转至授权服务器 Link to heading

构造授权 URL 并重定向:

GET https://auth.example.com/authorize?
    response_type=code
    &client_id=YOUR_CLIENT_ID
    &redirect_uri=https://yourapp.com/callback
    &scope=read:profile write:contacts
    &state=RANDOM_STATE_STRING
    &code_challenge=CHALLENGE_FROM_STEP1
    &code_challenge_method=S256

关键参数说明:

  • state:防 CSRF 攻击的随机串,回调时必须验证
  • code_challenge_method=S256:使用 SHA-256 算法

第三步:处理回调,用 code 换取 token Link to heading

用户同意授权后,授权服务器重定向回 redirect_uri,携带 codestate

GET https://yourapp.com/callback?code=AUTH_CODE&state=RANDOM_STATE_STRING

服务端验证 state 匹配后,用 code + code_verifier 换取 token:

import requests

def exchange_code_for_token(code, code_verifier):
    """用授权码换取 access_token"""
    response = requests.post("https://auth.example.com/oauth/token", data={
        "grant_type": "authorization_code",
        "code": code,
        "redirect_uri": "https://yourapp.com/callback",
        "client_id": "YOUR_CLIENT_ID",
        "code_verifier": code_verifier,   # PKCE 核心参数
    })

    token_data = response.json()
    return token_data

# 返回示例
# {
#   "access_token": "eyJhbGciOiJSUzI1NiIs...",
#   "token_type": "Bearer",
#   "expires_in": 3600,
#   "refresh_token": "dGhpcyBpcyBhIHJlZnJl...",
#   "scope": "read:profile write:contacts"
# }

第四步:携带 token 访问受保护资源 Link to heading

def fetch_user_profile(access_token):
    """使用 access_token 调用 API"""
    response = requests.get(
        "https://api.example.com/user/profile",
        headers={"Authorization": f"Bearer {access_token}"}
    )
    return response.json()

# 返回示例
# {"id": "12345", "name": "张三", "email": "zhangsan@example.com"}

第五步:token 过期后用 refresh_token 续期 Link to heading

def refresh_access_token(refresh_token):
    """用 refresh_token 获取新的 access_token"""
    response = requests.post("https://auth.example.com/oauth/token", data={
        "grant_type": "refresh_token",
        "refresh_token": refresh_token,
        "client_id": "YOUR_CLIENT_ID",
    })
    return response.json()

# 新返回会包含新的 access_token 和(可选的)新 refresh_token

Client Credentials 模式(M2M) Link to heading

服务器与服务器之间的授权,无需用户参与:

import requests

def get_m2m_token():
    """机器对机器认证"""
    response = requests.post("https://auth.example.com/oauth/token", data={
        "grant_type": "client_credentials",
        "client_id": "SERVICE_CLIENT_ID",
        "client_secret": "SERVICE_CLIENT_SECRET",
        "scope": "api:read api:write",
    })
    return response.json()

# 使用方式相同:Authorization: Bearer <token>

安全要点 Link to heading

风险防护措施
CSRF 攻击使用并验证 state 参数
授权码拦截启用 PKCE(RFC 7636)
token 泄露仅在 HTTPS 传输,服务端存储
token 被盗用设置合理的 expires_in,使用 short-lived token
redirect_uri 劫持在授权服务器预注册精确的回调地址
scope 越权最小权限原则,只申请必需的 scope

常见误区 Link to heading

  • OAuth ≠ 认证:OAuth 2.0 是授权框架,不是认证协议。如果需要"登录"功能,应使用基于 OAuth 2.0 的 OpenID Connect (OIDC),它增加了 id_token(JWT 格式)来标识用户身份。
  • Implicit 模式已废弃:RFC 6749 的 Implicit 模式因 token 暴露在 URL 中而被废弃,SPA 应改用 Authorization Code + PKCE。
  • access_token 不是身份凭证:不要用它来判断"用户是谁",这是 OIDC 的 id_token 的职责。

官方链接 Link to heading

[1] https://datatracker.ietf.org/doc/html/rfc6749

[2] https://datatracker.ietf.org/doc/html/rfc6750 (Bearer Token Usage)

[3] https://datatracker.ietf.org/doc/html/rfc7636 (PKCE)

[4] https://openid.net/connect/

Signature Link to heading

本文由 AI 生成,不保证正确,仅作参考