OAuth 流程图指南
怎么画一张真正有用的 OAuth 流程图:四个参与方、前后通道的区别,以及三个可直接改的例子 —— PKCE、刷新令牌轮换、机器对机器。
一、什么是 OAuth 流程图
OAuth 流程图 画的是:用户如何让一个应用访问自己在另一个服务上的数据,而不用把密码交出去。图上有四方,以及它们对话的顺序 —— 资源所有者(人)、客户端(你的应用)、授权服务器(发令牌的)、资源服务器(存数据的 API)。
把这四方叫对,就已经是这张图大半的价值。OAuth 图最常见的错误是把授权服务器和资源服务器并成一个叫「Google」的框。它们是两个系统、两份职责:一个决定 你是谁、你能干什么,另一个 提供数据、校验拿到的令牌。并成一个框之后,这张图就再也画不出最关键的那件事:谁会看到用户的密码(只有前者),谁自始至终只见过令牌(只有后者)。
第二个理由是 OAuth 是跳转协议,而跳转在代码里是隐形的。读一个 handler 只能知道某个端点发生了什么,读不出浏览器在三个源之间弹了三次、还带着一个必须原样返回的 state。时序图能把这几次弹跳画出来 —— 这就是为什么几乎所有真实的 OAuth bug(redirect_uri 对不上、state 丢了、code 被重放)在图上一目了然,在调用栈里完全看不见。
如果你看过 RFC 里那张图却还是画不出自己那版,这很正常:规范画的是抽象协议,你的图需要的恰好是规范留作练习的那些部分 —— 也就是下面三个例子。
二、一张 OAuth 流程图要画出哪些东西
五样。第一样就是「有用的 OAuth 图」和「照抄规范插图」的分水岭:
- 哪些箭头经过浏览器。 OAuth 有 前通道(用户浏览器跟着走的跳转,地址栏里看得见、历史记录里留得下)和 后通道(服务器之间的调用,谁也看不见)。协议里所有的设计决策都源自这个区分。一定要标出来 —— 虚线、注释、颜色,什么都行。
- 每条箭头带的是什么。
code、access_token、refresh_token、id_token是四样东西,四种生命周期、四种泄露后果。每条箭头都写「token」,等于把整个安全叙事全丢了。 - 授权服务器和资源服务器分成两条生命线,哪怕它们是同一家厂商在运营。理由见上一节。
- 至少一条拒绝分支。 code 过期、
state对不上、PKCE 校验不过、刷新令牌被吊销 —— 挑你们团队真的会争论的那一条,用alt画出来。 - 令牌最后落在哪。 停在「签发了 access token」的图只回答了一半问题。有意思的是另一半:客户端拿它怎么办 —— 放内存、放 cookie,还是放在一个不该放的地方。
这是骨架 —— 授权码流程,没有错误分支,四个参与方各占一条线。下面每个例子都是在它上面加一样具体的东西。
看 Mermaid 源码
sequenceDiagram
participant User as Resource Owner
participant App as Client App
participant Auth as Authorization Server
participant API as Resource Server
User->>App: click Sign in
App->>Auth: redirect to /authorize
Auth->>User: login and consent screen
User->>Auth: approve
Auth-->>App: redirect back with code
App->>Auth: POST /token with the code
Auth-->>App: access token
App->>API: GET /me with the access token
API-->>App: profile data三、OAuth 流程图怎么画
三步。大部分图错在第 1 步,而那一步只需要两分钟。
第 1 步 · 先确定你画的是哪个流程
「OAuth」不是一个流程,画一个笼统的版本,结果是对每个具体场景都不对。先回答一个问题 —— 签发令牌的那一刻,有人在场吗?
有人在场,你画的是 授权码 + PKCE。Web 应用、单页应用、移动端都是这个答案,自从 PKCE 对公开客户端成为强制要求以来一直是。如果你看到某篇教程画的是 隐式流程(令牌直接跟在跳转 URL 里返回),那是那之前的东西,不要抄。
没人在场 —— 定时任务、跟合作方 API 同步的服务 —— 你画的是 客户端凭据,图上根本没有用户那条生命线。这个「没有」本身就是信息:它解释了为什么没有授权页、也没有刷新令牌。
其他的(电视机上的设备码、服务之间的令牌交换)是你需要时自然会知道的变体。挑一个,只画那一个。一张图同时覆盖两个流程,等于两个都没覆盖。
第 2 步 · 把前通道和后通道分开
把箭头过一遍,分成两堆:浏览器带着走的 和 我们服务器直接调的。
分类本身是机械的 —— 凡是跳转、凡是会出现在用户地址栏里的 URL、凡是用户提交的表单,都是前通道 —— 但分完的结果就是这个协议的全部要点。前通道是可观察的:它在浏览器历史里、在 Referer 头里、在服务器日志里,还可能就在一台共用电脑的屏幕上。这正是为什么授权码走前通道而访问令牌不走。授权码短命、一次性、没有客户端密钥或 PKCE 校验值就没用 —— 它就是被设计成「被看见也没关系」的。
分完之后检查一件事:前通道上有没有什么东西,被身后的人瞄一眼就会出事? 有的话,那就是一个发现 —— 而这张图存在的意义就是产出这种发现。
第 3 步 · 画出来
把这套握手用句子描述出来,让 text2diagram 排版 ——「应用带着 code challenge 把浏览器跳转到授权服务器,用户登录并同意,服务器带着 code 跳回来,应用拿 code 加校验值去令牌端点换令牌」,还回来的是一段可以直接改的 sequenceDiagram。
或者把下面的例子在编辑器里打开,把参与方改成你的服务商。难的是箭头怎么排,Authorization Server → Okta 只是一次查找替换。
四、三个 OAuth 流程图例子
三个规范插图不会替你画的流程:现在的默认做法、令牌过期之后发生什么、以及完全没有用户时是什么样。
例 1 · 授权码 + PKCE
值得琢磨的是第一条和最后一条箭头。应用生成一个 code_verifier,只把它的 哈希(code_challenge)通过前通道发出去;到最后一步才把校验值本身通过后通道发过去。从跳转里偷到 code 的攻击者有 code 没有校验值,这次交换就失败 —— 这就是全部构思,而它在图上是看得懂的,在文字里不是。
还要注意 check state matches 那条自箭头。它不是网络调用,所以经常被省略;而省略它正是回调端点上的 CSRF 上线的方式。一个起防护作用的步骤值得有一个框,哪怕它不过网。
看 Mermaid 源码
sequenceDiagram
autonumber
participant User as Browser
participant App as Client App
participant Auth as Authorization Server
participant API as Resource Server
App->>App: generate code_verifier and code_challenge
App->>Auth: GET /authorize with client_id, redirect_uri, code_challenge, state
Auth->>User: login and consent
User->>Auth: approve
Auth-->>App: 302 back to redirect_uri with code and state
App->>App: check state matches what we sent
App->>Auth: POST /token with code and code_verifier
alt verifier does not match the challenge
Auth-->>App: 400 invalid_grant
else verifier matches
Auth-->>App: access_token and refresh_token
App->>API: GET /me with Bearer access_token
API-->>App: profile
end例 2 · 刷新令牌轮换与重用检测
这是团队跳过、然后凌晨两点来查的那条路径。访问令牌不停过期,所以这条路比上面的登录跑得频繁得多 —— 而它有一条大多数图都不画的分支。
开了 轮换 之后,每次刷新都作废旧令牌、发一个新的。这就把「刷新令牌被偷」变成一个可检测的事件:如果 RT1 被出示了两次,那么攻击者和真客户端里必有一个在用服务器已经作废的令牌,而服务器分不出是哪个。唯一安全的反应是把整个令牌家族全部吊销、逼用户重新登录。把第二条分支画出来,才让「我们做了刷新令牌轮换」从一个勾选框变成一个有明确后果的行为。
看 Mermaid 源码
sequenceDiagram
autonumber
participant App as Client App
participant Auth as Authorization Server
participant API as Resource Server
App->>API: GET /orders with Bearer access_token
API-->>App: 401 token expired
App->>Auth: POST /token with refresh token RT1
alt RT1 is unused and valid
Auth->>Auth: revoke RT1, issue RT2
Auth-->>App: new access_token and RT2
App->>API: retry GET /orders
API-->>App: 200 OK
else RT1 was already used - it leaked
Auth->>Auth: revoke the whole token family
Auth-->>App: 400 invalid_grant
App->>App: clear session, send the user to login
end例 3 · 客户端凭据 —— 完全没有用户
要注意的是这张图里 缺了什么:没有浏览器生命线、没有授权页、没有跳转、根本没有前通道。这里每条箭头都是服务器到服务器,这既解释了为什么这个流程可以直接用客户端密钥,也解释了为什么 PKCE 在这里无事可做。
值得画的是那条乏味的分支 —— 令牌在一个长批处理中途过期。它一直很乏味,直到某天夜里同步跑了一半,两个系统的数据对不上了,这时候所有人都想知道:这个任务是重新认证接着跑,还是就地放弃?图上一眼就能回答。
看 Mermaid 源码
sequenceDiagram
autonumber
participant Job as Nightly Job
participant Auth as Authorization Server
participant API as Partner API
Note over Job: no user is present - nobody to consent
Job->>Auth: POST /token with client_id and client_secret
Auth-->>Job: short lived access_token, scoped
Job->>API: POST /sync with Bearer access_token
alt token still valid
API-->>Job: 200 OK
else token expired mid batch
API-->>Job: 401
Job->>Auth: POST /token again
Auth-->>Job: fresh access_token
Job->>API: retry POST /sync
API-->>Job: 200 OK
end如果有一条箭头带着机密穿过浏览器,那就是这张图的结论
画完之后,只读前通道那几条箭头,逐条问:这一条要是被截图了,代价是什么?授权码 —— 按设计,没有代价。跟在 URL 片段里的访问令牌 —— 一个会话。客户端密钥出现在浏览器附近 —— 全部,而且它说明这个应用是个公开客户端,本来就该改用 PKCE。
这是 OAuth 图能让你廉价做完、而读代码几乎做不到的一次评审:前通道是散在一次跳转、一个回调路由和一个你控制不了的浏览器里的。
常见问题
OAuth 和 OpenID Connect 有什么区别?
OAuth 2.0 是授权 —— 它让你的应用拿到「代表某人调用某个 API」的权限。OpenID Connect 是认证 —— 架在 OAuth 上的一薄层,额外告诉你的应用 这个用户是谁,靠的是多返回一个
id_token(一个带身份信息的签名 JWT)。在图上,区别就是一条箭头的载荷:令牌端点如果在访问令牌之外还返回id_token,你画的就是 OIDC。「用 Google 登录」是 OIDC;「允许这个应用读你的 Google 日历」是纯 OAuth。我的图该画哪个 OAuth 流程?
有人在场就是授权码 + PKCE —— Web 应用、单页应用、移动端,三者都是。没人在场就是客户端凭据。这两个覆盖了绝大多数真实系统。不要画隐式流程:它把访问令牌放在跳转 URL 里返回,等于把一个活的凭据丢进浏览器历史和
Referer头,已经被劝退很多年了。如果你接手的图画的是它,那张图记录的是一个问题,不是一个设计。PKCE 是什么,为什么现在每张图上都有它?
PKCE(Proof Key for Code Exchange)是两个额外的值,用来证明「来兑换授权码的应用」和「当初发起请求的应用」是同一个。客户端生成一个随机的
code_verifier,把它的哈希(code_challenge)走前通道发出去,兑换时再把校验值本身走后通道发过去。它存在的原因是公开客户端 —— 单页应用和移动端 —— 保不住客户端密钥,没有它的话偷到一个 code 就足以换到令牌。现在连机密客户端也建议用,所以几乎所有当代的图上都有它。授权服务器和资源服务器要分开画吗?
要分开,哪怕是同一家厂商在跑。它们回答不同的问题 —— 一个签发和校验令牌,一个提供数据 —— 分开画才能让图表达出「资源服务器从头到尾没见过凭据,只见过一个它去校验的令牌」。合并还会藏起一个实际的运维事实:它们通常是不同的域名、各自会挂、各自限流。唯一的例外是你在做一张刻意抽象的概念图,教的是原理而不是记录某个系统。
令牌过期和刷新能画在同一张图里吗?
通常不该 —— 登录和刷新分成两张图。它们发生在不同时刻、频率差得极远,合在一起会得到一张长尾巴、没人看到底的图。登录那张画到「令牌已签发」为止,再画第二张,从「访问令牌已过期」开始,就像上面第二个例子。如果非要合,给刷新那段套一个
loop块,让读者看出这段是反复发生的,而不是一条一次性路径。免费吗?
免费。没登录每天 20 次,登录后每天 500 次。把本页任何一个例子在编辑器里打开完全不计次 —— 那条路径根本不打模型。