跳到主要内容

OAuth 2.1 授权协议

OAuth 2.1 是 OAuth 2.0 的演进版本,整合了过去十年的安全最佳实践。OAuth 是一套授权框架,解决的是"让第三方应用在不获取用户密码的情况下,代表用户访问受保护资源"的问题。

理解 OAuth 解决的问题

想象一个场景:你开发了一个日程管理应用,需要读取用户的 Google 日历数据。

最直接的做法是让用户把 Google 账号密码给你。但这有几个明显问题:

  • 用户需要把密码交给第三方应用,存在泄露风险
  • 第三方应用获得的权限过大,能做用户能做的一切事情
  • 用户无法精细控制授权范围
  • 用户修改密码后,所有使用密码的应用都需要重新授权

OAuth 通过引入授权服务器访问令牌解决了这些问题。用户在授权服务器上完成身份验证,然后授予第三方应用特定范围的权限,第三方应用获得一个受限的访问令牌。

核心角色

OAuth 定义了四个角色:

角色说明举例
资源拥有者用户本人Google 用户
客户端请求访问资源的应用日程管理应用
授权服务器验证用户身份并颁发令牌Google Accounts
资源服务器存储受保护数据的 APIGoogle 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 之前,确认以下问题:

  1. 是否需要第三方访问用户数据? 是则选择 OAuth
  2. 是否有后端服务器?
    • 有:使用授权码模式
    • 没有:使用授权码 + PKCE
  3. 是否需要用户身份信息? 是则在 OAuth 基础上使用 OIDC
  4. 是否配置了 state 参数? 必须配置以防御 CSRF
  5. 是否使用精确的重定向 URI? 不能使用通配符

小结

本章学习了 OAuth 2.1 的核心概念:

  1. 角色定义:资源拥有者、客户端、授权服务器、资源服务器
  2. 授权码模式:标准流程,安全性最高的授权方式
  3. PKCE:为公共客户端增强安全性
  4. 客户端凭据模式:服务器间通信场景
  5. 安全要点:state 参数、重定向验证、最小权限

练习

  1. 使用 GitHub OAuth API 实现用户登录功能
  2. 实现带 PKCE 的授权码流程(纯前端)
  3. 对比授权码模式和已废弃的隐式模式的安全差异

OAuth 2.1 与 OAuth 2.0 详细对比

OAuth 2.1 整合了过去十年 OAuth 2.0 的安全最佳实践,明确废弃了不安全的授权模式:

特性OAuth 2.0OAuth 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 中的最佳实践整合到核心规范中。即使规范仍处于草案状态,主流身份提供商和框架已经开始采纳这些安全要求。

参考资料