OAuth 2.0 流程图:一步步看懂带 PKCE 的授权码模式

用时序图讲解带 PKCE 的 OAuth 2.0 授权码流程:每次重定向、每个参数和令牌、各方要做的检查,以及应当弃用的旧流程。

· 创始人兼开发者

发布于

OAuth 2.0 流程图:一步步看懂带 PKCE 的授权码模式

OAuth 2.0 用起来容易,在脑子里画出来却很难。一个登录按钮会在三方或四方之间触发一连串重定向,每一步都带着名字相近的参数,而大多数 bug 都源于某一方跳过了本该做的检查。要看清谁在什么顺序下把什么发给了谁,时序图是最清楚的方式。

本文画出了如今几乎所有客户端都应该采用的流程:带 PKCE 的授权码模式。我们逐步讲解每一步,说明每一方要检查什么,并列出当前规范建议弃用的旧流程。

四个参与方

规范中的角色 实际对应 持有
资源所有者 用户 自己的凭据
客户端 你的 Web 应用、单页应用或移动应用 收到的令牌
授权服务器 你的身份提供商 用户账号、授权同意、签名密钥
资源服务器 你的 API 数据,以及接受哪些令牌的规则

客户端永远看不到用户的密码。这正是 OAuth 的意义所在:由授权服务器认证用户,再交给客户端一个范围和有效期都受限的令牌。

2026 年该用哪种流程

最初的 OAuth 2.0 框架 RFC 6749 定义了好几种授权模式。2025 年 1 月发布的 OAuth 2.0 安全最佳当前实践 RFC 9700 缩小了选择范围:

  • 带 PKCE 的授权码模式,用于任何类型客户端的用户登录。公开客户端(移动应用、单页应用)必须使用 PKCE;对于在服务器端持有密钥的机密客户端,也推荐使用。
  • 不要用隐式模式。 它在浏览器重定向中返回令牌,令牌可能在那里泄露;客户端不应该使用它。
  • 不要用资源所有者密码凭据模式。 把用户密码交给客户端违背了初衷;BCP 规定不得使用它。
  • 客户端凭据模式,用于没有用户参与的服务间调用。

OAuth 2.1 草案把这些规则整合进一份文档,所以新的库越来越多地默认遵循它们。

一段话讲清 PKCE

Proof Key for Code Exchange,RFC 7636,用来保护授权码。在重定向之前,客户端生成一个随机的 code_verifier(43 到 128 个字符),只把它的 SHA-256 哈希值,即 code_challenge,连同 code_challenge_method=S256 一起发出去。之后客户端用授权码换取令牌时,再发送 verifier。服务器对它做哈希并比较。截获了授权码的攻击者,没有从未离开客户端的 verifier,就无法使用这个授权码。

流程,一步一步

  1. 用户点击登录。 客户端生成 state(一个与用户会话绑定的随机值),以及 PKCE 的 verifier 和 challenge。
  2. 客户端重定向到 /authorize,带上 response_type=code、client_id、已注册的 redirect_uri、请求的 scope、state 和 code challenge。
  3. 授权服务器显示登录和授权同意页面。 客户端不参与;这一步发生在服务器的域名下。
  4. 用户登录,按需完成 MFA,并同意授权范围。
  5. 服务器重定向回来,回到与注册完全一致的 redirect_uri,带上一个短期有效、只能使用一次的 code,以及同一个 state。客户端检查 state 是否与自己发出的一致;不一致就停止。RFC 6749 建议授权码的有效期最多 10 分钟。
  6. 客户端向 /token 发送 POST 请求,带上授权码、重定向 URI 和 code_verifier(如果是机密客户端,还要带上客户端认证)。这是一个后端通道请求,而不是重定向。
  7. 服务器返回令牌: 一个访问令牌,通常还有一个刷新令牌;如果使用 OpenID Connect,还有一个 ID 令牌。
  8. 客户端调用 API,带上 Authorization: Bearer <access token>。
  9. API 校验令牌并响应。对于 JWT 访问令牌,它在本地检查签名、签发者、受众、过期时间和权限范围;对于不透明令牌,它会询问授权服务器的内省端点。
  10. 访问令牌过期时,客户端把刷新令牌 POST 到 /token。
  11. 服务器返回新的访问令牌,对公开客户端还会返回新的刷新令牌。RFC 9700 要求公开客户端的刷新令牌要么与发送方绑定,要么进行轮换,这样被盗的刷新令牌在任何一方使用一次之后就会失效。

时序图

一张包含用户、客户端应用、授权服务器和 API 的时序图。客户端带着 code challenge 和 state 把用户送去授权,拿回授权码,用授权码和 verifier 换取令牌,带 Bearer 令牌调用 API,之后再刷新令牌。

6 个组件 · 11 个连接

在 Archboard 中打开
图表内容
  • User
  • Client app
  • Authorization server
  • API (resource server)
  • access token expired
  • 1. Sign in
  • 2. GET /authorize + code_challenge, state
  • 3. Login and consent page
  • 4. Credentials, MFA, consent
  • 5. Redirect to redirect_uri with code, state
  • 6. POST /token: code + code_verifier
  • 7. Access token, refresh token
  • 8. GET /orders, Authorization: Bearer token
  • 9. 200 OK
  • 10. POST /token: refresh_token
  • 11. New access token (and rotated refresh token)
以时序图展示的带 PKCE 授权码流程。客户端在授权请求中发送 code challenge,在用授权码换取令牌时证明自己持有对应的 verifier,然后用访问令牌调用 API,直到需要刷新。

这张图的读法如下:

  • 四条生命线,从左到右:用户、客户端应用、授权服务器和 API。
  • 实心箭头的实线表示请求;虚线箭头表示响应和回跳的重定向。
  • 第 2 到 5 步是经由浏览器的前端通道重定向;第 6 步是后端通道的令牌请求,PKCE 正是在这里发挥作用。
  • 底部的 opt 片段只在访问令牌过期时才会执行。
  • Markdown 块列出了每一方要做的检查,真正的 bug 大多藏在这里。

评审中常见的错误

  • 不检查 state。 没有这项检查,攻击者就能在受害者的浏览器中,用攻击者自己的账号完成一次登录流程。
  • 重定向 URI 匹配过于宽松。 服务器必须与注册的 URI 精确比较;前缀匹配或通配符匹配会让授权码被盗。
  • 单页应用把令牌放在 local storage 里。 任何脚本注入都能读到它们。很多团队在 SPA 前面加一个小后端(backend-for-frontend),由它保存令牌,再给浏览器一个会话 cookie。
  • API 接受任何有效令牌。 要检查受众,否则为另一个 API 签发的令牌在你的 API 上也能用。
  • 移动应用中内嵌的 Web 视图。 RFC 8252 指出原生应用应该使用系统浏览器,这样应用就看不到用户的凭据。
  • 为了省掉刷新逻辑而使用长期有效的访问令牌。 让访问令牌保持短期有效并定期刷新;这样撤销才能很快生效。

OAuth 图上应该画什么

  • 每个参与方各占一条生命线,如果你的客户端有后端,浏览器也要单独画出来。
  • 区分前端通道和后端通道,因为它们的安全属性不同。
  • 在箭头上写参数名: state、code_challenge、code、code_verifier。评审者会找这些。
  • 谁校验什么。 检查才是设计本身;箭头只是在搬运数据。

如果你要画更大范围的安全全景,零信任架构图 展示了身份提供商在其他安全控制之间的位置,API 网关架构图 则介绍了如何在边缘统一校验一次令牌。时序图示例 更详细地解释了这里使用的表示法。

在 Archboard 中构建

  1. 使用 Google 或 GitHub 登录,并新建一张图。
  2. 从时序节点包中添加一个 参与者 表示用户,再添加三个 生命线 节点,分别表示客户端应用、授权服务器和 API,排成一行,高度相同。
  3. 把每条消息画成生命线之间的直线连线,从上到下排列,并在标签中写上参数名。响应和重定向用空心箭头的虚线连线。
  4. 在刷新步骤外面添加一个带 opt 运算符的 组合片段。
  5. 添加一个基础节点包中的 Markdown 块,写明每一方要做的检查。

这个示例共用 6 个图表节点,在 Free 套餐的限制之内。

常见问题

OAuth 2.0 和 OpenID Connect 有什么区别? OAuth 2.0 关注的是委托访问:客户端拿到令牌去调用 API。OpenID Connect 在此之上增加了身份:一个告诉客户端用户是谁的 ID 令牌,以及一个标准的用户信息端点。

我的应用有客户端密钥,还需要 PKCE 吗? RFC 9700 也推荐机密客户端使用。它能防御授权码注入,代价只是每一端多一个参数。

还应该使用隐式模式吗? 不应该。当前的最佳实践规定客户端不应使用它。改用带 PKCE 的授权码模式,单页应用也一样。

单页应用应该把令牌存在哪里? 最好根本不放在浏览器里:backend-for-frontend 可以在服务器端保存令牌,再给浏览器一个安全的会话 cookie。如果令牌必须放在浏览器中,就只放在内存里,让访问令牌保持短期有效,并使用刷新令牌轮换。

API 如何校验访问令牌? 对于 JWT 访问令牌,用授权服务器公布的密钥校验签名,同时检查签发者、受众、过期时间和权限范围。对于不透明令牌,调用内省端点。

免费开始 — 1 个席位,无需信用卡。

相关文章

← 返回博客

一分钟打开画布

使用 Google 或 GitHub 登录。单席位免费,团队加入后再升级。