OAuth 2.0
OAuth 2.0 是现代 Web 应用中实现第三方授权的行业标准协议。它允许用户授权第三方应用访问其资源,而无需共享密码。
为什么需要 OAuth 2.0?
在传统的应用授权模式中,用户需要将自己的用户名和密码提供给第三方应用,让第三方应用代表自己访问资源。这种方式存在严重的安全隐患:
- 密码泄露风险 - 第三方应用需要存储用户密码
- 权限过度授予 - 第三方应用获得用户的全部权限
- 无法撤销权限 - 用户无法单独撤销某个应用的访问权限,只能修改密码
- 密码修改影响 - 用户修改密码后,所有第三方应用都无法正常工作
OAuth 2.0 通过引入授权层,将客户端应用的角色与资源所有者的角色分离,解决了这些问题。
核心概念
四种角色
资源所有者(Resource Owner)
- 能够授予对受保护资源访问权限的实体
- 通常就是终端用户
资源服务器(Resource Server)
- 托管受保护资源的服务器
- 能够接受和响应使用访问令牌的资源请求
客户端(Client)
- 代表资源所有者发起受保护资源请求的应用程序
- 例如想要访问用户照片的手机应用或网站
授权服务器(Authorization Server)
- 在成功验证资源所有者并获得授权后,向客户端颁发访问令牌的服务器
协议流程
+--------+ +---------------+
| |--(A)- Authorization Request ->| Resource |
| | | Owner |
| |<-(B)-- Authorization Grant ---| |
| | +---------------+
| |
| | +---------------+
| |--(C)-- Authorization Grant -->| Authorization |
| Client | | Server |
| |<-(D)----- Access Token -------| |
| | +---------------+
| |
| | +---------------+
| |--(E)----- Access Token ------>| Resource |
| | | Server |
| |<-(F)--- Protected Resource ---| |
+--------+ +---------------+
流程说明:
- (A) 客户端向资源所有者请求授权
- (B) 资源所有者同意授权,返回授权许可
- (C) 客户端使用授权许可向授权服务器请求访问令牌
- (D) 授权服务器验证并颁发访问令牌
- (E) 客户端使用访问令牌向资源服务器请求受保护资源
- (F) 资源服务器验证访问令牌,返回受保护资源
授权许可类型
1. 授权码模式(Authorization Code Grant)
最常用、最安全的授权模式,适用于有后端的 Web 应用。
适用场景:
- 传统的服务器端 Web 应用
- 需要长期访问令牌的应用
- 对安全性要求高的应用
流程详解:
+----------+
| Resource |
| Owner |
| |
+----------+
^
|
(B)
+----|-----+ Client Identifier +---------------+
| -+----(A)-- & Redirection URI ---->| |
| User- | | Authorization |
| Agent -+----(B)-- User authenticates --->| Server |
| | | |
| -+----(C)-- Authorization Code ---| |
+-|----|---+ +---------------+
| | ^ v
(A) (C) | |
| | | |
^ v | |
+---------+ | |
| |>---(D)-- Authorization Code ---------' |
| Client | & Redirection URI |
| | |
| |<---(E)----- Access Token -------------------'
+---------+ (w/ Optional Refresh Token)
步骤说明:
- (A) 客户端将用户代理重定向到授权服务器
- (B) 授权服务器验证资源所有者身份,资源所有者同意授权
- (C) 授权服务器将用户代理重定向回客户端,携带授权码
- (D) 客户端使用授权码向授权服务器请求访问令牌
- (E) 授权服务器验证授权码和客户端身份,返回访问令牌
2. 简化模式(Implicit Grant)
⚠️ 已被标记为废弃,不推荐使用。请使用授权码模式配合 PKCE。
简化模式直接在浏览器中返回访问令牌,没有授权码交换步骤。由于令牌暴露在 URL 中,存在安全风险。
3. 密码凭证模式(Resource Owner Password Credentials Grant)
用户直接向客户端提供用户名和密码,客户端使用这些凭据向授权服务器请求令牌。
适用场景:
- 受信任的应用(如第一方移动应用)
- 遗留系统的迁移
- 不推荐用于新应用
4. 客户端凭证模式(Client Credentials Grant)
客户端使用自己的凭据(而非用户的凭据)向授权服务器请求令牌。
适用场景:
- 后台服务之间的 API 调用
- 数据同步任务
- 不需要用户参与的自动化流程
PKCE 扩展
PKCE(Proof Key for Code Exchange)是授权码模式的扩展,用于防止授权码拦截攻击。对于公共客户端(如移动应用、单页应用)是必需的。
PKCE 流程
+--------+ +---------------+
| |--(A)- Authorization Request --| Resource |
| | + code_challenge | Owner |
| | +---------------+
| |<-(B)----- Authorization Grant ----------------|
| | + code |
| Client | +---------------+
| |--(C)----- Access Token Request -------------->| Authorization |
| | + code_verifier | Server |
| |<-(D)----- Access Token -----------------------| |
+--------+ +---------------+
步骤说明:
- 客户端生成
code_verifier(随机字符串) - 计算
code_challenge(code_verifier的 SHA-256 哈希值) - 授权请求时发送
code_challenge - 交换令牌时发送
code_verifier - 授权服务器验证
code_verifier生成的 challenge 是否匹配
令牌类型
Access Token(访问令牌)
- 用于访问受保护资源的凭证
- 短期有效(通常 1 小时)
- 包含用户授权范围(Scope)信息
Refresh Token(刷新令牌)
- 用于获取新的 Access Token
- 长期有效(通常 7-30 天)
- 比 Access Token 更安全,使用频率低
┌─────────┐ ┌─────────┐
│ Client │ │ Server │
└────┬────┘ └────┬────┘
│ │
│ Access Token 过期 (401) │
│ <────────────────────────────────────── │
│ │
│ 使用 Refresh Token 请求新令牌 │
│ ──────────────────────────────────────> │
│ │
│ 返回新的 Access Token │
│ <────────────────────────────────────── │
Scope(授权范围)
Scope 用于限制客户端的访问权限,实现最小权限原则。
常见 Scope 示例:
| Scope | 说明 |
|---|---|
read | 读取权限 |
write | 写入权限 |
email | 访问邮箱地址 |
profile | 访问基本资料 |
openid | OpenID Connect 认证 |
授权界面示例:
应用 "XXX" 请求访问您的以下信息:
☑ 读取您的基本资料
☑ 读取您的邮箱地址
☐ 发布内容到您的账号
[同意] [拒绝]
安全最佳实践
1. 验证 State 参数
State 参数用于防止 CSRF 攻击。客户端在授权请求时生成随机 state,授权服务器在回调时返回相同的 state,客户端验证是否匹配。
2. 验证重定向 URI
必须严格验证重定向 URI,只允许预注册的 URI,防止授权码被重定向到恶意网站。
3. 使用 HTTPS
所有 OAuth 2.0 通信必须使用 HTTPS,防止令牌被截获。
4. 公共客户端使用 PKCE
移动应用、单页应用等公共客户端必须使用 PKCE 扩展。
5. 安全的令牌存储
- Access Token:内存存储或短期 Cookie
- Refresh Token:安全存储(Keychain、Keystore 或 HttpOnly Cookie)
常见安全漏洞
| 漏洞 | 风险 | 防护措施 |
|---|---|---|
| CSRF 攻击 | 恶意网站获取授权 | 使用 state 参数 |
| 授权码拦截 | 授权码被窃取 | 使用 PKCE |
| 重定向 URI 篡改 | 授权码发送到恶意网站 | 严格验证重定向 URI |
| 令牌泄露 | 敏感信息泄露 | 使用 HTTPS、安全存储 |
| 范围过大 | 过度授权 | 最小权限原则 |
OAuth 2.0 vs OpenID Connect
| 特性 | OAuth 2.0 | OpenID Connect |
|---|---|---|
| 主要目的 | 授权 | 认证 |
| 令牌类型 | Access Token | Access Token + ID Token |
| 用户信息 | 通过 API 获取 | 包含在 ID Token 中 |
| 标准化 | 授权协议 | 认证层(基于 OAuth 2.0) |
| 使用场景 | 第三方授权 | 单点登录、身份认证 |
OpenID Connect 在 OAuth 2.0 之上添加了身份认证层,通过 ID Token 提供标准化的用户信息。
代码实现
Node.js (Passport.js)
const passport = require('passport');
const GoogleStrategy = require('passport-google-oauth20').Strategy;
passport.use(new GoogleStrategy({
clientID: process.env.GOOGLE_CLIENT_ID,
clientSecret: process.env.GOOGLE_CLIENT_SECRET,
callbackURL: '/auth/google/callback'
},
async (accessToken, refreshToken, profile, done) => {
const user = await User.findOrCreate({ googleId: profile.id });
return done(null, user);
}
));
app.get('/auth/google', passport.authenticate('google', {
scope: ['profile', 'email']
}));
app.get('/auth/google/callback',
passport.authenticate('google', { failureRedirect: '/login' }),
(req, res) => res.redirect('/dashboard')
);
Spring Boot (Java)
@Configuration
@EnableWebSecurity
public class SecurityConfig {
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http.oauth2Login(oauth2 -> oauth2
.loginPage("/login")
.defaultSuccessUrl("/dashboard")
);
return http.build();
}
}
Python (Authlib)
from authlib.integrations.flask_client import OAuth
oauth = OAuth(app)
oauth.register(
name='google',
client_id=os.getenv('GOOGLE_CLIENT_ID'),
client_secret=os.getenv('GOOGLE_CLIENT_SECRET'),
server_metadata_url='https://accounts.google.com/.well-known/openid-configuration',
client_kwargs={'scope': 'openid email profile'}
)
@app.route('/login/google')
def google_login():
return oauth.google.authorize_redirect(url_for('google_callback', _external=True))
@app.route('/auth/google/callback')
def google_callback():
token = oauth.google.authorize_access_token()
userinfo = token.get('userinfo')
return redirect('/dashboard')
小结
-
核心要点
- 授权码模式是最安全、最常用的模式
- 公共客户端必须使用 PKCE
- 所有通信必须使用 HTTPS
-
安全实践
- 验证 state 参数防止 CSRF
- 严格验证重定向 URI
- 安全存储 client_secret
- 实施适当的令牌生命周期
- 遵循最小权限原则
-
适用场景
- 第三方登录(微信、GitHub、Google 等)
- API 开放平台
- 企业内部系统集成
- 单点登录(配合 OpenID Connect)