OAuth 认证协议
OAuth 2.0 授权流程与前端实现
问题
说一说 OAuth 协议,它是如何工作的?
解答
OAuth 是一个开放标准的授权协议,允许用户授权第三方应用访问其在另一个服务上的资源,而无需暴露密码。
OAuth 2.0 角色
- Resource Owner:资源所有者,即用户
- Client:第三方应用
- Authorization Server:授权服务器,颁发 token
- Resource Server:资源服务器,存放用户资源
授权码模式(最常用)
+--------+ +---------------+
| |---(1) 授权请求 --------------->| 用户代理 |
| | | (浏览器) |
| |<--(2) 授权码 ------------------| |
| | +---------------+
| Client |
| | +---------------+
| |---(3) 授权码换 Token --------->| Authorization |
| |<--(4) Access Token -----------| Server |
| | +---------------+
| |
| | +---------------+
| |---(5) 携带 Token 请求资源 ---->| Resource |
| |<--(6) 返回资源 ---------------| Server |
+--------+ +---------------+
前端实现示例
// 1. 发起授权请求
function initiateOAuth() {
const params = new URLSearchParams({
client_id: 'your_client_id',
redirect_uri: 'https://yourapp.com/callback',
response_type: 'code',
scope: 'read:user',
state: generateRandomState() // 防止 CSRF
});
// 跳转到授权页面
window.location.href = `https://auth.example.com/authorize?${params}`;
}
// 2. 回调页面处理授权码
function handleCallback() {
const params = new URLSearchParams(window.location.search);
const code = params.get('code');
const state = params.get('state');
// 验证 state 防止 CSRF
if (state !== localStorage.getItem('oauth_state')) {
throw new Error('State mismatch');
}
// 用授权码换取 token(通常由后端完成)
return exchangeCodeForToken(code);
}
// 3. 授权码换 Token(后端实现更安全)
async function exchangeCodeForToken(code) {
const response = await fetch('https://auth.example.com/token', {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded'
},
body: new URLSearchParams({
grant_type: 'authorization_code',
code,
client_id: 'your_client_id',
client_secret: 'your_client_secret', // 应在后端处理
redirect_uri: 'https://yourapp.com/callback'
})
});
return response.json();
// 返回: { access_token, refresh_token, expires_in, token_type }
}
// 4. 使用 Token 请求资源
async function fetchUserInfo(accessToken) {
const response = await fetch('https://api.example.com/user', {
headers: {
'Authorization': `Bearer ${accessToken}`
}
});
return response.json();
}
四种授权模式
| 模式 | 适用场景 | 安全性 |
|---|---|---|
| 授权码模式 | 有后端的 Web 应用 | 最高 |
| 隐式模式 | 纯前端 SPA(已不推荐) | 较低 |
| 密码模式 | 高度信任的应用 | 中等 |
| 客户端凭证模式 | 服务端对服务端 | 高 |
PKCE 扩展(SPA 推荐)
// 生成 code_verifier 和 code_challenge
async function generatePKCE() {
// 生成随机字符串作为 code_verifier
const verifier = generateRandomString(64);
// 计算 code_challenge = BASE64URL(SHA256(code_verifier))
const encoder = new TextEncoder();
const data = encoder.encode(verifier);
const hash = await crypto.subtle.digest('SHA-256', data);
const challenge = base64UrlEncode(hash);
return { verifier, challenge };
}
// 授权请求时带上 code_challenge
function initiateOAuthWithPKCE(codeChallenge) {
const params = new URLSearchParams({
client_id: 'your_client_id',
redirect_uri: 'https://yourapp.com/callback',
response_type: 'code',
scope: 'read:user',
state: generateRandomState(),
code_challenge: codeChallenge,
code_challenge_method: 'S256'
});
window.location.href = `https://auth.example.com/authorize?${params}`;
}
// 换取 token 时带上 code_verifier
async function exchangeWithPKCE(code, codeVerifier) {
const response = await fetch('https://auth.example.com/token', {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded'
},
body: new URLSearchParams({
grant_type: 'authorization_code',
code,
client_id: 'your_client_id',
redirect_uri: 'https://yourapp.com/callback',
code_verifier: codeVerifier // 无需 client_secret
})
});
return response.json();
}
关键点
- OAuth 是授权协议,不是认证协议;OpenID Connect 基于 OAuth 实现认证
- 授权码模式最安全,token 交换在后端完成,避免暴露 client_secret
- state 参数用于防止 CSRF 攻击,必须验证
- SPA 应用使用 PKCE 扩展,无需 client_secret 也能安全授权
- Access Token 有效期短,Refresh Token 用于续期
目录