Skip to main content

RFC 6749 - OAuth 2.0授权框架

文档信息

  • RFC编号: 6749
  • 标题: The OAuth 2.0 Authorization Framework
  • 标题(中文): OAuth 2.0授权框架
  • 发布日期: 2012年10月
  • 作者: D. Hardt (Microsoft)
  • 状态: 标准轨道 (Standards Track)
  • 更新: RFC 7636 (PKCE), RFC 8252 (Native Apps)

摘要

OAuth 2.0授权框架使第三方应用能够获得对HTTP服务的有限访问权限。它通过协调资源所有者和HTTP服务之间的审批交互,或允许第三方应用代表自己获得访问权限。OAuth 2.0是OAuth 1.0的完全重写,不向后兼容。

核心概念

传统授权模式的问题

问题场景:用户想让打印服务访问云存储中的照片

传统方式(不安全)

1. 用户给打印服务提供云存储的用户名和密码
2. 打印服务使用密码访问云存储

问题:
❌ 打印服务获得了完全访问权限
❌ 打印服务可以访问所有文件,不仅是照片
❌ 用户无法撤销访问(除非改密码)
❌ 任何第三方应用泄露都会暴露密码
❌ 难以追踪谁在访问数据

OAuth 2.0方式(安全)

1. 用户在云存储授权打印服务访问
2. 云存储给打印服务颁发访问令牌
3. 打印服务使用令牌访问照片

优势:
✅ 打印服务从不知道用户密码
✅ 访问权限受限(仅照片,只读)
✅ 可以随时撤销令牌
✅ 令牌可以过期
✅ 每个应用独立授权

OAuth 2.0角色

四个核心角色

┌─────────────────────────────────────────────────┐
│ 资源所有者 (Resource Owner) │
│ (用户) │
└────────────┬────────────────────────┬───────────┘
│ │
│ (1)授权请求 │ (2)授权许可
│ │
↓ ↓
┌────────────────────┐ (3)授权许可 ┌──────────────────┐
│ 客户端 │─────────────────>│ 授权服务器 │
│ (Client) │ │ (Authorization │
│ (第三方应用) │<─────────────────│ Server) │
│ │ (4)访问令牌 │ (云存储授权) │
└────────────────────┘ └──────────────────┘
│ │
│ (5)访问令牌 │
│ │
↓ │
┌────────────────────────────────────────────────┐│
│ 资源服务器 (Resource Server) ││
│ (云存储API) ││
└────────────────────────────────────────────────┘│
│ │
│ (6)受保护的资源 │
│ │
└─────────────────────────────────────┘

角色说明

  1. Resource Owner(资源所有者) - 通常是终端用户

    • 拥有受保护资源的实体
    • 能够授予对资源的访问权限
  2. Client(客户端) - 第三方应用

    • 代表资源所有者请求访问受保护资源
    • 可以是Web应用、移动应用、桌面应用等
  3. Authorization Server(授权服务器)

    • 认证资源所有者
    • 获得授权后颁发访问令牌
  4. Resource Server(资源服务器)

    • 托管受保护资源
    • 接受并验证访问令牌
    • 响应受保护资源请求

四种授权模式

OAuth 2.0定义了四种获取访问令牌的方式:

1. 授权码模式(Authorization Code)⭐ 最安全

适用场景:Web服务器应用(有后端)

流程图

客户端                      浏览器              授权服务器         资源服务器
│ │ │ │
│ 1.重定向到授权页面 │ │ │
│─────────────────────────>│ │ │
│ │ 2.授权请求 │ │
│ │────────────────────>│ │
│ │ │ 3.显示登录页面 │
│ │<────────────────────│ │
│ │ 4.用户登录并授权 │ │
│ │────────────────────>│ │
│ │ 5.重定向+code │ │
│ 6.接收code │<────────────────────│ │
│<─────────────────────────│ │ │
│ │ │ │
│ 7.用code换token │ │
│──────────────────────────────────────────────>│ │
│ │ │ │
│ 8.返回access_token │ │
│<──────────────────────────────────────────────│ │
│ │ │ │
│ 9.使用token访问API │
│─────────────────────────────────────────────────────────────────>│
│ │ │ │
│ 10.返回受保护资源 │
│<─────────────────────────────────────────────────────────────────│

示例代码

// 第1步:重定向到授权页面
const authUrl = new URL('https://auth.example.com/oauth/authorize');
authUrl.searchParams.append('response_type', 'code');
authUrl.searchParams.append('client_id', 'YOUR_CLIENT_ID');
authUrl.searchParams.append('redirect_uri', 'https://yourapp.com/callback');
authUrl.searchParams.append('scope', 'read write');
authUrl.searchParams.append('state', generateRandomState());

window.location.href = authUrl.toString();

// 第2步:处理回调,接收授权码
app.get('/callback', async (req, res) => {
const { code, state } = req.query;

// 验证state防止CSRF
if (state !== req.session.state) {
return res.status(403).send('Invalid state');
}

// 第3步:用授权码换取访问令牌
const tokenResponse = await fetch('https://auth.example.com/oauth/token', {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
'Authorization': 'Basic ' + btoa(`${CLIENT_ID}:${CLIENT_SECRET}`)
},
body: new URLSearchParams({
grant_type: 'authorization_code',
code: code,
redirect_uri: 'https://yourapp.com/callback'
})
});

const tokens = await tokenResponse.json();
// {
// access_token: "eyJhbGc...",
// token_type: "Bearer",
// expires_in: 3600,
// refresh_token: "tGzv3JOkF0XG5Qx2TlKWIA",
// scope: "read write"
// }

// 保存令牌
req.session.accessToken = tokens.access_token;
res.redirect('/dashboard');
});

// 第4步:使用访问令牌访问API
app.get('/api/photos', async (req, res) => {
const response = await fetch('https://api.example.com/photos', {
headers: {
'Authorization': `Bearer ${req.session.accessToken}`
}
});

const photos = await response.json();
res.json(photos);
});

2. 隐式模式(Implicit)⚠️ 已弃用

注意:此模式已不推荐使用,应使用"授权码+PKCE"代替。

原始用途:单页应用(SPA)

问题

  • ❌ 令牌在URL中暴露
  • ❌ 无刷新令牌
  • ❌ 容易受到令牌泄露

流程(仅供了解):

客户端 → 授权服务器: 授权请求
授权服务器 → 客户端: 直接返回access_token(在URL片段中)

现代替代方案:使用授权码模式 + PKCE(RFC 7636)

3. 资源所有者密码凭证模式(Password)⚠️ 谨慎使用

适用场景:高度可信的应用(如官方应用)

流程

客户端 → 授权服务器: 直接发送username + password
授权服务器 → 客户端: 返回access_token

示例

const tokenResponse = await fetch('https://auth.example.com/oauth/token', {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded'
},
body: new URLSearchParams({
grant_type: 'password',
username: '[email protected]',
password: 'user_password',
client_id: 'YOUR_CLIENT_ID',
client_secret: 'YOUR_CLIENT_SECRET',
scope: 'read write'
})
});

const tokens = await tokenResponse.json();

使用限制

  • ⚠️ 仅用于第一方应用
  • ⚠️ 用户必须绝对信任客户端
  • ⚠️ 不建议用于第三方集成
  • ⚠️ 应尽快迁移到其他模式

4. 客户端凭证模式(Client Credentials)

适用场景:机器对机器(M2M)通信,无用户参与

流程

客户端 → 授权服务器: client_id + client_secret
授权服务器 → 客户端: access_token

示例

// 微服务间调用
const tokenResponse = await fetch('https://auth.example.com/oauth/token', {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
'Authorization': 'Basic ' + btoa(`${CLIENT_ID}:${CLIENT_SECRET}`)
},
body: new URLSearchParams({
grant_type: 'client_credentials',
scope: 'api.read api.write'
})
});

const { access_token } = await tokenResponse.json();

// 使用令牌调用API
const apiResponse = await fetch('https://api.example.com/data', {
headers: {
'Authorization': `Bearer ${access_token}`
}
});

适用场景

  • ✅ 后端服务间调用
  • ✅ CLI工具
  • ✅ 守护进程
  • ✅ 定时任务

刷新令牌(Refresh Token)

为什么需要刷新令牌?

问题:访问令牌会过期(通常1小时)
过期后用户需要重新登录?

解决方案:使用刷新令牌获取新的访问令牌
无需用户再次授权!

访问令牌:短期有效(1小时),用于访问API
刷新令牌:长期有效(数周/数月),仅用于获取新访问令牌

刷新令牌流程

客户端                              授权服务器
│ │
│ 使用access_token访问API │
│ ────────────> [过期!] │
│ │
│ POST /token │
│ grant_type=refresh_token │
│ refresh_token=xxx │
│─────────────────────────────────────>│
│ │
│ 新的access_token │
│ 新的refresh_token (可选)│
│<─────────────────────────────────────│
│ │
│ 使用新token继续访问API │
│ ────────────> [成功!] │

实现示例

class OAuth2Client {
constructor(config) {
this.accessToken = null;
this.refreshToken = null;
this.expiresAt = null;
this.config = config;
}

async getAccessToken() {
// 检查令牌是否需要刷新
if (this.needsRefresh()) {
await this.refreshAccessToken();
}

return this.accessToken;
}

needsRefresh() {
if (!this.accessToken) return true;
if (!this.expiresAt) return false;

// 提前5分钟刷新
const buffer = 5 * 60 * 1000;
return Date.now() >= (this.expiresAt - buffer);
}

async refreshAccessToken() {
if (!this.refreshToken) {
throw new Error('No refresh token available');
}

const response = await fetch(this.config.tokenUrl, {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
'Authorization': 'Basic ' + btoa(
`${this.config.clientId}:${this.config.clientSecret}`
)
},
body: new URLSearchParams({
grant_type: 'refresh_token',
refresh_token: this.refreshToken
})
});

if (!response.ok) {
// 刷新失败,清除令牌,需要重新授权
this.clearTokens();
throw new Error('Token refresh failed');
}

const tokens = await response.json();
this.updateTokens(tokens);
}

updateTokens(tokens) {
this.accessToken = tokens.access_token;

if (tokens.refresh_token) {
this.refreshToken = tokens.refresh_token;
}

if (tokens.expires_in) {
this.expiresAt = Date.now() + (tokens.expires_in * 1000);
}

// 持久化存储
this.saveTokens();
}

async apiCall(url, options = {}) {
const token = await this.getAccessToken();

const response = await fetch(url, {
...options,
headers: {
...options.headers,
'Authorization': `Bearer ${token}`
}
});

// 处理401,尝试刷新后重试
if (response.status === 401) {
await this.refreshAccessToken();
return this.apiCall(url, options);
}

return response;
}
}

// 使用
const client = new OAuth2Client({
clientId: 'YOUR_CLIENT_ID',
clientSecret: 'YOUR_CLIENT_SECRET',
tokenUrl: 'https://auth.example.com/oauth/token'
});

// 自动处理令牌刷新
const response = await client.apiCall('https://api.example.com/data');

访问令牌类型

Bearer Token(最常用)

GET /api/resource HTTP/1.1
Host: api.example.com
Authorization: Bearer eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9...

特点

  • 简单易用
  • 任何拥有令牌的人都可以使用(Bearer = "持有者")
  • 必须通过HTTPS传输
  • 应设置合理的过期时间

MAC Token(较少使用)

使用消息认证码(MAC)保护令牌:

GET /api/resource HTTP/1.1
Host: api.example.com
Authorization: MAC id="h480djs93hd8",
ts="137131200",
nonce="dj83hs9s",
mac="bhCQXTVyfj5cmA9uKkPFx1zeOXM="

作用域(Scope)

作用域用于限制访问权限:

// 请求特定权限
const authUrl = new URL('https://auth.example.com/oauth/authorize');
authUrl.searchParams.append('scope', 'read:photos write:photos delete:photos');

// 用户可以批准部分权限
// 返回的令牌可能只有: "read:photos write:photos"

常见作用域设计

资源:操作模式:
- read:photos 只读照片
- write:photos 创建/修改照片
- delete:photos 删除照片

角色模式:
- admin 管理员权限
- user 普通用户权限
- guest 访客权限

OpenID Connect:
- openid 必需,表示OpenID Connect请求
- profile 访问用户基本信息
- email 访问用户邮箱
- address 访问用户地址

错误处理

授权错误

HTTP/1.1 302 Found
Location: https://client.example.com/callback?
error=access_denied
&error_description=The%20user%20denied%20access
&state=xyz

常见错误码

错误码说明
invalid_request请求缺少必需参数或参数无效
unauthorized_client客户端无权使用此授权模式
access_denied资源所有者拒绝授权
unsupported_response_type不支持的响应类型
invalid_scope请求的作用域无效或未知
server_error服务器内部错误
temporarily_unavailable服务器暂时不可用

令牌错误

{
"error": "invalid_grant",
"error_description": "The authorization code has expired"
}

常见令牌错误

错误码说明
invalid_request请求格式错误
invalid_client客户端认证失败
invalid_grant授权码/刷新令牌无效或过期
unauthorized_client客户端无权使用此授权模式
unsupported_grant_type不支持的授权类型

API调用错误

HTTP/1.1 401 Unauthorized
WWW-Authenticate: Bearer realm="example",
error="invalid_token",
error_description="The access token expired"

安全最佳实践

1. 使用HTTPS

⚠️ OAuth 2.0必须在HTTPS上运行

原因:
- 令牌在HTTP头部传输
- 防止中间人攻击
- 保护用户凭证

2. 验证重定向URI

// 服务器端严格验证
const allowedRedirectUris = [
'https://app.example.com/callback',
'https://app.example.com/oauth/callback'
];

if (!allowedRedirectUris.includes(requestedRedirectUri)) {
return res.status(400).json({
error: 'invalid_request',
error_description: 'Invalid redirect_uri'
});
}

3. 使用State参数防止CSRF

// 生成并存储state
const state = crypto.randomBytes(32).toString('hex');
req.session.oauthState = state;

// 授权请求包含state
const authUrl = `...&state=${state}`;

// 回调时验证
if (req.query.state !== req.session.oauthState) {
throw new Error('CSRF attack detected');
}

4. 限制令牌作用域

// 只请求必要的权限
scope: 'read:user read:repo' // ✅ 最小权限

scope: 'admin' // ❌ 过度权限

5. 安全存储令牌

// ✅ 服务器端:加密存储
const encryptedToken = encrypt(accessToken, ENCRYPTION_KEY);
await db.saveToken(userId, encryptedToken);

// ✅ 浏览器:httpOnly cookie
res.cookie('token', accessToken, {
httpOnly: true,
secure: true,
sameSite: 'strict',
maxAge: 3600000
});

// ❌ 避免:localStorage(易受XSS攻击)
localStorage.setItem('token', accessToken);

6. 定期轮换刷新令牌

// 每次刷新都返回新的refresh_token
{
"access_token": "new_access_token",
"refresh_token": "new_refresh_token", // 新的!
"expires_in": 3600
}

// 旧的refresh_token失效

完整实现示例

Express + Passport OAuth2

const express = require('express');
const passport = require('passport');
const OAuth2Strategy = require('passport-oauth2');
const session = require('express-session');

const app = express();

// 配置session
app.use(session({
secret: 'your-secret-key',
resave: false,
saveUninitialized: false,
cookie: { secure: true, httpOnly: true }
}));

app.use(passport.initialize());
app.use(passport.session());

// 配置OAuth2策略
passport.use('oauth2', new OAuth2Strategy({
authorizationURL: 'https://auth.example.com/oauth/authorize',
tokenURL: 'https://auth.example.com/oauth/token',
clientID: process.env.CLIENT_ID,
clientSecret: process.env.CLIENT_SECRET,
callbackURL: 'https://yourapp.com/auth/callback',
scope: ['read', 'write']
},
async (accessToken, refreshToken, profile, done) => {
try {
// 获取用户信息
const user = await getUserProfile(accessToken);

// 保存令牌
await saveTokens(user.id, {
accessToken,
refreshToken,
expiresAt: Date.now() + 3600000
});

done(null, user);
} catch (error) {
done(error);
}
}
));

// 序列化用户
passport.serializeUser((user, done) => {
done(null, user.id);
});

passport.deserializeUser(async (id, done) => {
try {
const user = await findUserById(id);
done(null, user);
} catch (error) {
done(error);
}
});

// 路由
app.get('/auth/login', passport.authenticate('oauth2'));

app.get('/auth/callback',
passport.authenticate('oauth2', { failureRedirect: '/login' }),
(req, res) => {
res.redirect('/dashboard');
}
);

app.get('/auth/logout', (req, res) => {
req.logout(() => {
res.redirect('/');
});
});

// 受保护的路由
app.get('/api/data', ensureAuthenticated, async (req, res) => {
const tokens = await getTokens(req.user.id);

// 检查令牌是否过期
if (Date.now() >= tokens.expiresAt) {
tokens = await refreshAccessToken(tokens.refreshToken);
}

// 调用API
const response = await fetch('https://api.example.com/data', {
headers: {
'Authorization': `Bearer ${tokens.accessToken}`
}
});

const data = await response.json();
res.json(data);
});

function ensureAuthenticated(req, res, next) {
if (req.isAuthenticated()) {
return next();
}
res.redirect('/auth/login');
}

app.listen(3000);

总结

OAuth 2.0是现代Web授权的标准:

  • : 准确实现安全授权
  • : 清晰的四种授权模式
  • : 优雅的令牌机制

核心价值

  1. 🔐 无需共享密码
  2. 📱 支持多种客户端类型
  3. ⚖️ 细粒度权限控制
  4. 🔄 令牌刷新机制
  5. 🛡️ 可撤销的访问权限

选择指南

  • Web应用: 授权码模式 ✅
  • 移动应用: 授权码模式 + PKCE ✅
  • SPA: 授权码模式 + PKCE ✅
  • 服务间: 客户端凭证模式 ✅
  • 第一方: 密码模式 ⚠️
  • 任何场景: 隐式模式 ❌ 已弃用

相关RFC

  • RFC 6749: OAuth 2.0核心(本文档)
  • RFC 6750: OAuth 2.0 Bearer Token
  • RFC 7636: PKCE扩展
  • RFC 8252: OAuth for Native Apps
  • RFC 7519: JWT