OAuth 2.0 流程图
四种 grant_type · 时序图 · 实战代码
四种 grant 流程图 + 解释
四种 grant_type · 时序图 · 实战代码
了解工具定位 · 使用场景 · 对比优势
独立开发者或小团队在实现「微信登录」或「GitHub 登录」时,面对授权码模式(Authorization Code)和隐式模式(Implicit)的选择常感困惑。本工具通过可视化流程图,清晰展示两种模式下浏览器、后端服务器与授权服务器之间的跳转与令牌传递路径,帮助开发者快速理解 state 参数防 CSRF 的生效位置,避免因流程理解错误导致的安全漏洞或实现返工。
移动 App 开发者在调试「一次登录,多端可用」的 SSO 场景时,常因无法直观看到授权码在 App 内嵌 WebView 与系统浏览器之间的流转而反复抓包。本工具以时序图形式呈现授权码模式在移动端的完整流程,标明 redirect_uri 的注册与匹配规则,让开发者一眼看出为何某些回调地址会导致「无法完成登录」的报错,从而快速定位配置问题。
SaaS 平台架构师在设计「用户授权第三方应用访问其数据」的功能时,需要区分客户端凭证模式(Client Credentials)与授权码模式的使用边界。本工具通过对比流程图,直观展示前者适用于服务器到服务器的无用户场景,后者适用于有用户参与的授权场景,帮助架构师在微服务间调用与用户数据委托两种需求中做出正确的 grant type 选择,避免权限过度暴露。
安全工程师在审计已有 OAuth 2.0 实现时,需要逐一核对令牌端点是否验证 client_secret、授权码是否一次性使用、刷新令牌是否绑定到特定客户端。本工具将四种 grant 流程拆解为可交互的步骤图,审计人员可以对照每一步的 RFC 规范描述,快速标记出实现中缺失的校验环节,例如「授权码模式缺少 PKCE 扩展」或「隐式模式未限制 response_type」,从而生成精准的修复清单。
后端开发者在设计长期有效的 API 访问时,需要决定刷新令牌(Refresh Token)的过期时间与轮换策略。本工具展示授权码模式中刷新令牌的完整生命周期流程,并标注「一次使用」与「轮换」两种策略在序列图中的差异,帮助开发者直观理解为何轮换策略能降低令牌泄露的持续风险,以及如何结合过期时间设置实现自动续期与安全退出的平衡。
| 维度 | 本工具 (oauth-flow.tl654.com) | Auth0 在线调试器 | 手工搭建 (Spring Security / Passport.js) |
|---|---|---|---|
| 数据隐私 | 纯浏览器,流程图和令牌均在本地生成,无任何数据上传 | 令牌和授权码在 Auth0 服务器上生成和验证 | 令牌在自建服务器上生成,需自行保障服务器安全 |
| 处理速度 | 毫秒级,流程图与令牌生成完全在浏览器内完成 | 依赖网络请求,通常 1-3 秒 | 依赖服务器响应与网络延迟,通常 0.5-5 秒 |
| 离线可用 | 完全离线,无需网络连接 | 必须联网 | 必须联网,且需运行后端服务 |
| 上手成本 | 零配置,打开页面即可选择 Grant 类型并生成流程 | 需要注册 Auth0 账号并创建应用 | 需搭建开发环境、配置依赖库、编写路由代码,数小时到数天 |
| Grant 类型覆盖 | 四种核心 Grant(Authorization Code、Implicit、Client Credentials、Resource Owner Password Credentials) | 支持所有 Grant,并额外支持 PKCE、Device Code 等扩展 | 取决于框架,通常需手动实现或引入第三方库 |
| 令牌可视化 | 流程图实时高亮令牌流转路径,附带令牌结构解析 | 返回 JSON 格式令牌,需自行理解字段含义 | 返回原始令牌,需配合文档或工具解码 |
| 适用场景 | 学习 OAuth 2.0 流程、快速验证 Grant 逻辑、教学演示 | 生产环境集成测试、OAuth 服务端调试 | 生产级应用开发、自定义安全策略 |
上手步骤 · 输入输出 · 避坑提示
| 输入 | 输出 | 说明 |
|---|---|---|
| grant_type=authorization_code&code=SplxlOBeZQQYbYS6WxSbIA&redirect_uri=https://client.example.com/cb&client_id=s6BhdRkqt3 | 显示授权码流程(Authorization Code Grant)的完整步骤图:1. 用户浏览器访问授权端点 → 2. 用户登录并授权 → 3. 授权服务器返回授权码 → 4. 客户端用授权码向令牌端点请求 → 5. 返回 access_token(+refresh_token)。 | 典型场景:最常用的 OAuth 2.0 流程,用于 Web 应用 |
| grant_type=client_credentials&client_id=s6BhdRkqt3&client_secret=secret123 | 显示客户端凭证流程(Client Credentials Grant)的步骤图:1. 客户端直接向令牌端点请求 → 2. 返回 access_token(无 refresh_token)。 | 典型场景:服务器到服务器的 API 调用,无用户参与 |
| grant_type=password&username=johndoe&password=A3ddj3w&client_id=s6BhdRkqt3 | 显示密码流程(Resource Owner Password Credentials Grant)的步骤图:1. 用户向客户端提供用户名密码 → 2. 客户端向令牌端点请求 → 3. 返回 access_token(+refresh_token)。 | 典型场景:受信任的客户端(如官方 App)直接收集凭据 |
| grant_type=implicit&response_type=token&client_id=s6BhdRkqt3&redirect_uri=https://client.example.com/cb&scope=openid%20profile | 显示隐式流程(Implicit Grant)的步骤图:1. 用户浏览器访问授权端点 → 2. 用户登录并授权 → 3. 授权服务器直接返回 access_token(在 URL 片段中)。 | 边界 case:已不推荐使用(安全风险),仅用于纯前端应用 |
| grant_type=authorization_code&code=invalid_code_12345&redirect_uri=https://client.example.com/cb&client_id=s6BhdRkqt3 | 错误响应:{"error":"invalid_grant","error_description":"The provided authorization grant is invalid, expired, or revoked."} | 易错 case:授权码过期或伪造,新手常忽略有效期 |
| grant_type=refresh_token&refresh_token=tGzv3JOkF0XG5Qx2TlKWIA&client_id=s6BhdRkqt3&client_secret=secret123 | 显示刷新令牌流程:1. 客户端用 refresh_token 向令牌端点请求 → 2. 返回新的 access_token(+新的 refresh_token)。 | 边界 case:access_token 过期后,用 refresh_token 无感续期 |
| grant_type=authorization_code&code=SplxlOBeZQQYbYS6WxSbIA&redirect_uri=https://attacker.com/cb&client_id=s6BhdRkqt3 | 错误响应:{"error":"invalid_grant","error_description":"The redirect_uri does not match the registered redirect_uri."} | 易错 case:redirect_uri 不匹配,新手常忽略注册时的精确匹配 |
在纯前端 SPA 中使用授权码模式(Authorization Code),且无后端参与,直接暴露 client_secret。SPA 使用授权码模式 + PKCE(Proof Key for Code Exchange),或使用隐式模式(Implicit Grant,已不推荐)。授权码模式要求 client_secret 保密,纯前端无法安全存储;PKCE 用 code_verifier 替代 secret,适合无后端场景。
请求参数包含 `scope=user_info` 或 `login_hint=user@example.com`。客户端凭证模式只传 `grant_type=client_credentials` 和 `scope=api_read`(仅限应用级别权限)。此模式用于机器对机器通信,不涉及具体用户;传入用户信息会被服务端忽略或报错。
`state=123456`(固定值)或完全省略 state 参数。`state=random_generated_nonce`(每次请求生成唯一随机值,并在回调中验证)。state 用于防御 CSRF 攻击;固定值或缺失导致攻击者可伪造回调劫持用户会话。
用户复制包含 `#access_token=...` 的 URL 分享给他人,或浏览器历史记录中保留该 URL。使用后立即调用 `window.location.hash = ''` 清除片段,并告知用户不分享回调 URL。URL 片段中的 token 会被浏览器历史、Referer 头泄露;隐式模式本身安全性较低,需额外防护。
客户端将用户密码以 Base64 编码后直接存入本地 localStorage。密码仅用于单次 token 请求,获取 access_token 后立即丢弃密码,不持久化。密码模式要求客户端高度可信;持久化密码违背最小权限原则,一旦客户端被攻破,所有凭据泄露。
每次刷新都使用同一个 refresh_token,且不轮换(rotation)。使用 refresh_token 获取新 access_token 后,丢弃旧 refresh_token,使用新返回的 refresh_token。RFC 6749 推荐 refresh_token 轮换;固定 token 被窃取后可无限期使用,轮换可限制泄露窗口。
注册时填写 `https://example.com/callback`,但实际请求中传入 `https://evil.com/callback`。服务端必须精确匹配注册的 redirect_uri(包括路径、端口、协议),拒绝任何未注册的 URI。开放重定向允许攻击者截获授权码或 token;严格校验是 OAuth 2.0 安全基础之一。
授权码模式请求中写 `grant_type=authorization_code` 但实际是第一步获取 code 的请求。第一步授权请求用 `response_type=code`;第二步 token 请求用 `grant_type=authorization_code`。grant_type 用于 token 端点(第二步),response_type 用于授权端点(第一步);混用导致协议错误。
公式推导 · 流程图解 · 依据出处
授权码流程:code → token(通过 client_id + client_secret + redirect_uri 交换)
code — 授权码,临时凭证,有效期短client_id — 客户端标识,注册时分配client_secret — 客户端密钥,仅后端持有redirect_uri — 回调地址,必须与注册一致access_token — 访问令牌,用于调用资源 API用户授权后,授权服务器返回 code=abc123。客户端用 POST 请求向 /token 端点发送:client_id=myapp&client_secret=secret&code=abc123&redirect_uri=https://example.com/callback&grant_type=authorization_code。服务器验证通过后返回 access_token=eyJhbGciOiJIUzI1NiIs...(JWT 格式),有效期 3600 秒。
适用于有后端服务器的 Web 应用(机密客户端)。纯前端 SPA 或移动端无法安全保存 client_secret,应使用 PKCE 扩展流程。基于 IETF RFC 6749 标准。
3 种主流语言 · 复制即用
import requests
from urllib.parse import urlencode, urljoin
# 授权码模式(Authorization Code)客户端示例
CLIENT_ID = "your_client_id"
CLIENT_SECRET = "your_client_secret"
REDIRECT_URI = "https://your-app.com/callback"
AUTH_ENDPOINT = "https://provider.com/oauth2/authorize"
TOKEN_ENDPOINT = "https://provider.com/oauth2/token"
# 1. 构造授权请求 URL(用户浏览器跳转)
params = {
"response_type": "code",
"client_id": CLIENT_ID,
"redirect_uri": REDIRECT_URI,
"scope": "openid profile",
"state": "random_state_string" # 防 CSRF
}
auth_url = f"{AUTH_ENDPOINT}?{urlencode(params)}"
print(f"Redirect user to: {auth_url}")
# 2. 回调后,用授权码交换令牌(服务端执行)
authorization_code = "code_from_callback" # 从 query 参数获取
token_data = {
"grant_type": "authorization_code",
"code": authorization_code,
"redirect_uri": REDIRECT_URI,
"client_id": CLIENT_ID,
"client_secret": CLIENT_SECRET
}
response = requests.post(TOKEN_ENDPOINT, data=token_data)
if response.status_code == 200:
token = response.json()
print(f"Access token: {token['access_token']}")
print(f"Refresh token: {token.get('refresh_token')}")
else:
print(f"Token exchange failed: {response.text}")package main
import (
"context"
"fmt"
"golang.org/x/oauth2"
)
func main() {
// 客户端凭证模式(Client Credentials)—— 服务到服务认证
conf := &oauth2.Config{
ClientID: "your_client_id",
ClientSecret: "your_client_secret",
Scopes: []string{"api:read"},
Endpoint: oauth2.Endpoint{
TokenURL: "https://provider.com/oauth2/token",
},
}
// 直接获取令牌,无需用户交互
token, err := conf.TokenSource(context.Background(), &oauth2.Token{}).Token()
if err != nil {
fmt.Printf("Failed to get token: %v\n", err)
return
}
fmt.Printf("Access token: %s\n", token.AccessToken)
fmt.Printf("Token type: %s\n", token.TokenType)
fmt.Printf("Expires in: %v\n", token.Expiry)
// 使用令牌访问受保护资源
client := conf.Client(context.Background(), token)
resp, err := client.Get("https://api.provider.com/v1/resource")
if err != nil {
fmt.Printf("API call failed: %v\n", err)
return
}
defer resp.Body.Close()
fmt.Printf("API response status: %s\n", resp.Status)
}// 隐式模式(Implicit)—— 纯前端 SPA 示例(已不推荐,仅作演示)
// 实际生产应使用授权码 + PKCE
// 1. 生成 PKCE 验证码和挑战(推荐替代隐式模式)
function generatePKCE() {
const verifier = Array.from(crypto.getRandomValues(new Uint8Array(32)))
.map(b => b.toString(16).padStart(2, '0'))
.join('');
// 生成 SHA-256 挑战
return crypto.subtle.digest('SHA-256', new TextEncoder().encode(verifier))
.then(hash => {
const challenge = btoa(String.fromCharCode(...new Uint8Array(hash)))
.replace(/\+/g, '-')
.replace(/\//g, '_')
.replace(/=+$/, '');
return { verifier, challenge };
});
}
// 2. 构造授权请求(PKCE 增强的授权码模式)
async function startAuth() {
const { verifier, challenge } = await generatePKCE();
// 保存验证码到 localStorage(回调时使用)
localStorage.setItem('pkce_verifier', verifier);
const params = new URLSearchParams({
response_type: 'code',
client_id: 'your_client_id',
redirect_uri: 'https://your-app.com/callback',
code_challenge: challenge,
code_challenge_method: 'S256',
state: crypto.randomUUID(),
scope: 'openid profile'
});
window.location.href = `https://provider.com/oauth2/authorize?${params}`;
}
// 3. 回调处理:用授权码 + PKCE 验证码换令牌
async function exchangeCode(code) {
const verifier = localStorage.getItem('pkce_verifier');
localStorage.removeItem('pkce_verifier');
const response = await fetch('https://provider.com/oauth2/token', {
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
body: new URLSearchParams({
grant_type: 'authorization_code',
code: code,
redirect_uri: 'https://your-app.com/callback',
client_id: 'your_client_id',
code_verifier: verifier
})
});
if (!response.ok) throw new Error('Token exchange failed');
const token = await response.json();
console.log('Access token:', token.access_token);
return token;
}8 个高频疑问
「HTTP / 网络速查」下的其他工具