OAuth 2.0 是目前最主流的授权框架,允许第三方应用在无需获取用户密码的前提下,安全地访问受保护的资源。
假设读者具备基础的 HTTP 和 Web 开发经验。
核心概念 Link to heading
四个角色 Link to heading
| 角色 | 说明 | 示例 |
|---|---|---|
| Resource Owner | 资源所有者(用户) | 登录 Google 的张三 |
| Client | 请求资源的第三方应用 | “用 Google 登录” 的博客网站 |
| Authorization Server | 授权服务器,验证用户并发放 Token | accounts.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 Code | Web 服务端应用(推荐) | 最高 |
| Implicit | 单页应用(SPA,已废弃) | 低 |
| Resource Owner Password Credentials | 高度信任的应用(已废弃) | 中 |
| Client Credentials | 机器对机器(M2M) | 高 |
现代应用主要使用 Authorization Code + PKCE 模式,也是本文重点。
关键术语 Link to heading
- access_token:访问令牌,调用 API 的"钥匙",有有效期
- refresh_token:刷新令牌,用于获取新的 access_token
- scope:权限范围,如
read:profile、write: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,携带 code 和 state:
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 生成,不保证正确,仅作参考