OAuth 2.1 授权协议
OAuth 2.1 是 OAuth 2.0 的演进版本,整合了过去十年的安全最佳实践。OAuth 是一套授权框架,解决的是"让第三方应用在不获取用户密码的情况下,代表用户访问受保护资源"的问题。
理解 OAuth 解决的问题
想象一个场景:你开发了一个日程管理应用,需要读取用户的 Google 日历数据。
最直接的做法是让用户把 Google 账号密码给你。但这有几个明显问题:
- 用户需要把密码交给第三方应用,存在泄露风险
- 第三方应用获得的权限过大,能做用户能做的一切事情
- 用户无法精细控制授权范围
- 用户修改密码后,所有使用密码的应用都需要重新授权
OAuth 通过引入授权服务器和访问令牌解决了这些问题。用户在授权服务器上完成身份验证,然后授予第三方应用特定范围的权限,第三方应用获得一个受限的访问令牌。
核心角色
OAuth 定义了四个角色:
| 角色 | 说明 | 举例 |
|---|---|---|
| 资源拥有者 | 用户本人 | Google 用户 |
| 客户端 | 请求访问资源的应用 | 日程管理应用 |
| 授权服务器 | 验证用户身份并颁发令牌 | Google Accounts |
| 资源服务器 | 存储受保护数据的 API | Google Calendar API |
授权码模式
授权码模式是 OAuth 2.1 推荐的标准流程,适用于有后端服务器的应用。
流程步骤
第一步:引导用户授权
客户端将用户重定向到授权服务器:
https://accounts.google.com/o/oauth2/v2/auth?
response_type=code&
client_id=YOUR_CLIENT_ID&
redirect_uri=https://yourapp.com/callback&
scope=calendar.readonly&
state=random_string
参数说明:
response_type=code:表示使用授权码模式client_id:客户端在授权服务器注册时获得的标识redirect_uri:用户授权后跳转回的地址scope:请求的权限范围state:防 CSRF 攻击的随机字符串
第二步:用户授权
用户在授权服务器的页面上看到应用请求的权限,选择同意或拒绝。
同意后,授权服务器跳转回 redirect_uri 并附带授权码:
https://yourapp.com/callback?code=AUTH_CODE&state=random_string
第三步:交换访问令牌
客户端后端使用授权码向授权服务器请求访问令牌:
POST https://oauth2.googleapis.com/token
Content-Type: application/x-www-form-urlencoded
code=AUTH_CODE&
client_id=YOUR_CLIENT_ID&
client_secret=YOUR_CLIENT_SECRET&
redirect_uri=https://yourapp.com/callback&
grant_type=authorization_code
授权服务器验证通过后,返回访问令牌:
{
"access_token": "ya29.xxx",
"token_type": "Bearer",
"expires_in": 3600,
"refresh_token": "1//xxx",
"scope": "calendar.readonly"
}
第四步:访问资源
客户端使用访问令牌调用资源服务器的 API:
GET https://www.googleapis.com/calendar/v3/calendars/primary/events
Authorization: Bearer ya29.xxx
为什么需要授权码
授权码模式比直接返回令牌多了一步,但安全性更高:
- 令牌只在后端服务器之间传输,不经过浏览器地址栏
- 即使授权码被截获,没有 client_secret 也无法换取令牌
- 后端服务器能保管 client_secret,而前端应用无法做到
PKCE 增强安全
对于没有后端的应用(如单页应用、移动应用),无法安全保管 client_secret。PKCE(Proof Key for Code Exchange)机制解决了这个问题。
PKCE 工作原理
客户端生成一个随机字符串 code_verifier,然后计算其哈希值 code_challenge:
// 生成 code_verifier
const codeVerifier = generateRandomString(128);
// 计算 code_challenge(SHA256 后 Base64Url 编码)
const encoder = new TextEncoder();
const data = encoder.encode(codeVerifier);
const digest = await crypto.subtle.digest('SHA-256', data);
const codeChallenge = base64UrlEncode(digest);
授权请求时携带 code_challenge:
response_type=code&
code_challenge=CODE_CHALLENGE&
code_challenge_method=S256
交换令牌时携带原始的 code_verifier:
code=AUTH_CODE&
code_verifier=CODE_VERIFIER
授权服务器验证 code_verifier 是否与之前的 code_challenge 匹配。即使授权码被截获,攻击者没有 code_verifier 也无法换取令牌。
OAuth 2.1 要求所有授权码流程必须使用 PKCE,不再仅限于公共客户端。
其他授权模式
客户端凭据模式
适用于两个服务器之间的通信,没有用户参与:
POST https://auth.example.com/token
Content-Type: application/x-www-form-urlencoded
grant_type=client_credentials&
client_id=SERVICE_A&
client_secret=SERVICE_A_SECRET&
scope=api.read
典型场景:微服务之间的内部调用。
已废弃的模式
以下模式在 OAuth 2.1 中已被废弃或禁止:
| 模式 | 状态 | 原因 |
|---|---|---|
| 隐式模式 | 严格禁止 | 令牌暴露在 URL 中,易被截获 |
| 密码模式 | 已废弃 | 客户端需要接触用户密码 |
如果你的应用还在使用隐式模式或密码模式,应该尽快迁移到授权码 + PKCE 模式。
安全配置要点
State 参数
state 参数用于防御 CSRF 攻击。客户端在发起授权请求时生成随机值,授权完成后验证返回的 state 是否一致:
import secrets
import hmac
# 生成 state
state = secrets.token_urlsafe(32)
# 存储 state(如存入 session)
session['oauth_state'] = state
# 验证回调中的 state
if request.args['state'] != session['oauth_state']:
raise SecurityError('State 参数不匹配')
重定向 URI 验证
- 必须使用精确匹配,不能使用通配符
- 不要接受
http://localhost以外的 HTTP 地址 - 注册最小必要数量的重定向 URI
Scope 最小权限
只请求应用真正需要的权限,遵循最小权限原则:
# 不好:请求过多权限
scope=calendar email profile drive
# 好:只请求需要的权限
scope=calendar.readonly
各语言实现示例
Python(Authlib)
from authlib.integrations.flask_client import OAuth
oauth = OAuth(app)
oauth.register(
name='github',
client_id='YOUR_CLIENT_ID',
client_secret='YOUR_CLIENT_SECRET',
access_token_url='https://github.com/login/oauth/access_token',
authorize_url='https://github.com/login/oauth/authorize',
api_base_url='https://api.github.com/',
)
@app.route('/login')
def login():
redirect_uri = url_for('callback', _external=True)
return oauth.github.authorize_redirect(redirect_uri)
@app.route('/callback')
def callback():
token = oauth.github.authorize_access_token()
resp = oauth.github.get('user')
return jsonify(resp.json())
Node.js(passport)
const passport = require('passport');
const GoogleStrategy = require('passport-google-oauth20').Strategy;
passport.use(new GoogleStrategy({
clientID: GOOGLE_CLIENT_ID,
clientSecret: GOOGLE_CLIENT_SECRET,
callbackURL: "https://yourapp.com/auth/google/callback"
},
(accessToken, refreshToken, profile, done) => {
// 处理用户信息
return done(null, profile);
}
));
app.get('/auth/google',
passport.authenticate('google', { scope: ['profile', 'calendar.readonly'] })
);
app.get('/auth/google/callback',
passport.authenticate('google', { failureRedirect: '/login' }),
(req, res) => { res.redirect('/dashboard'); }
);
Spring Security(Java)
application.yml 配置:
spring:
security:
oauth2:
client:
registration:
github:
client-id: YOUR_CLIENT_ID
client-secret: YOUR_CLIENT_SECRET
scope: user:email
OpenID Connect
OAuth 2.0/2.1 只解决授权问题,不提供用户身份信息的标准化格式。OpenID Connect(OIDC)在 OAuth 之上增加了身份认证层:
- 提供标准化的 ID Token(JWT 格式)
- 定义了用户信息端点(UserInfo Endpoint)
- 支持发现机制(通过
.well-known/openid-configuration获取配置)
如果你的应用需要获取用户的姓名、邮箱等身份信息,应该使用 OIDC 而不是自己从 OAuth 获取。
选型检查表
在使用 OAuth 之前,确认以下问题:
- 是否需要第三方访问用户数据? 是则选择 OAuth
- 是否有后端服务器?
- 有:使用授权码模式
- 没有:使用授权码 + PKCE
- 是否需要用户身份信息? 是则在 OAuth 基础上使用 OIDC
- 是否配置了
state参数? 必须配置以防御 CSRF - 是否使用精确的重定向 URI? 不能使用通配符
小结
本章学习了 OAuth 2.1 的核心概念:
- 角色定义:资源拥有者、客户端、授权服务器、资源服务器
- 授权码模式:标准流程,安全性最高的授权方式
- PKCE:为公共客户端增强安全性
- 客户端凭据模式:服务器间通信场景
- 安全要点:state 参数、重定向验证、最小权限
练习
- 使用 GitHub OAuth API 实现用户登录功能
- 实现带 PKCE 的授权码流程(纯前端)
- 对比授权码模式和已废弃的隐式模式的安全差异
OAuth 2.1 与 OAuth 2.0 详细对比
OAuth 2.1 整合了过去十年 OAuth 2.0 的安全最佳实践,明确废弃了不安全的授权模式:
| 特性 | OAuth 2.0 | OAuth 2.1 | 变化说明 |
|---|---|---|---|
| 授权码模式 | 可选 PKCE | 强制 PKCE | 所有授权码流程必须使用 PKCE 保护 |
| 隐式模式 | 允许 | 已废弃 | 令牌暴露在 URL 中,存在安全风险 |
| 密码模式 | 允许 | 已废弃 | 客户端接触用户密码,违背 OAuth 设计初衷 |
| 重定向 URI | 允许通配符 | 精确匹配 | 必须完全匹配,不能使用通配符 |
| Bearer Token 位置 | 允许查询字符串 | 禁止查询字符串 | 只能在 Header 或 POST Body 中传输 |
| Refresh Token | 无限制 | 单次使用或发送者约束 | 必须使用 DPoP 或 mTLS 绑定,或单次使用后失效 |
| 原生应用 | 多种模式 | 统一使用授权码 + PKCE | 简化实现,提高安全性 |
| 浏览器应用 | 隐式模式 | 授权码 + PKCE | 更安全的令牌获取方式 |
迁移建议
如果你的应用正在使用已废弃的模式,应该尽快迁移:
- 隐式模式 → 授权码模式 + PKCE
- 密码模式 → 授权码模式 + PKCE 或客户端凭据模式(服务器间通信)
OAuth 2.1 不引入新功能,而是将分散在多个 RFC 中的最佳实践整合到核心规范中。即使规范仍处于草案状态,主流身份提供商和框架已经开始采纳这些安全要求。