原文《An Illustrated Guide to OAuth》,作者 Aditya Bhargava,发布于 2025年8月25日
链接 www.ducktyped.org/p/an-illust...
阮老师于《科技爱好者周刊#363》推荐了此文。
转载及翻译已获原作者授权。

OAuth 于 2007 年首次推出。
它最初由 Twitter 创建,因为 Twitter 希望能够允许第三方应用代表用户发布推文。
想象一下,如果今天设计类似的功能,你会怎么做?一种方法是直接要求用户输入用户名和密码。因此,你创建了一个非官方的 Twitter 客户端,并向用户展示一个登录页面,上面写着"使用 Twitter 登录"。
用户照做了,但他们实际上并没有登录 Twitter,而是将数据发送给了你------这个替他们登录 Twitter 的第三方服务。

这种做法有很多弊端。即使你信任第三方应用,但如果它们没有正确存储你的密码,导致有人窃取了你的密码,该怎么办?所以你绝对不应该把密码透露给这样的第三方网站。
你可能会想到另一种办法:用 API key。毕竟你要调用 Twitter 的 API 来替用户发数据,而调用 API 就得用 API key。可问题是,API key 通常是整个应用共用的,但你真正需要的,是一个能代表某个具体用户的密钥。

为了解决这些问题,OAuth 应运而生。你会看到它是如何解决所有这些问题的,OAuth 的核心就是 Access Token,这玩意儿就像是专属某个用户的 API key。应用获取 Access Token 后,就可以代表用户执行操作,或访问用户的数据。
OAuth 的工作原理
OAuth 的使用方式多种多样,这也是它让人头疼难懂的原因之一。在本文中,我们将探讨一个典型的 OAuth 流程。
我将使用 YNAB 来举例(一个理财软件,有点像 Mint 的付费版)。
当你把它连到银行账户,它会拉取所有交易,用很漂亮的图表展示,还能帮你分类消费,比如提醒你"买食品花太多了"。它就是个财务管理工具。

我想用 YNAB 连我的 Chase 银行账户(美国的大通银行),但我又不想把 Chase 的密码给它,所以我决定用 OAuth。
我们先过一下 OAuth 的流程,然后再弄清楚背后具体发生了什么。实际上,我们会把流程过两遍,因为我认为至少需要两遍才能真正明白它。
OAuth 流程,第一遍
首先,从 YNAB 开始,我想把 Chase 当数据源连接进来。OAuth 流程如下:
- YNAB 把我重定向到 Chase。
- 在 Chase,我使用用户名和密码登录。
- Chase 弹出个页面:"YNAB 想访问你的 Chase,请选择你要授权的账户。" 然后展示出我所有的账户列表。比如我只选支票账户,只给读取权限,点确认。
- 然后 Chase 把我重定向回 YNAB。神奇的是,YNAB 已经接上 Chase 了。

这就是用户角度看到的。但背后到底发生了什么,让 YNAB 能访问我的 Chase 数据?
最终目标是获得 Access Token
记住,OAuth 的最终目的,就是让 YNAB 拿到一个 Access Token,好去访问我的 Chase 数据。 在这个流程里,YNAB 最终确实拿到了 token。为了避免你被绕晕,我先告诉你 YNAB 是怎样拿到 Access Token 的,然后再详细拆开流程。
关于安全的简短说明
Chase 怎么把 Access Token 给 YNAB 呢? 一种简单做法是:直接在重定向 URL 里带上 token,比如:
ini
https://www.ynab.com/redirect?access_token=123
但这是个 极烂的主意!
Access Token 应该是保密的,但 URL 会出现在浏览器历史、服务器日志里,这么一来谁都能轻易拿走你的 token。
所以实际上,Chase 重定向回 YNAB 时,放的不是 token,而是一个 authorization code(授权码) 。
授权码 ≠ Access Token。
YNAB 再用这个授权码,发一个后端的 HTTPS POST 请求给 Chase,去换取真正的 Access Token。这样数据在后端传输,别人就看不到了。

于是 YNAB 就拿到了 token,OAuth 流程结束,安全收官。
OAuth 的两个部分

回顾一下,其实 OAuth 流程大概分为两个部分:
- 用户授权流程:用户登录,选择授权范围(scopes)。这很关键,因为 OAuth 强调用户必须明确参与和掌控。
- 授权码流程:这是 YNAB 实际获取 Access Token 的流程。
接下来我们会更详细的讨论 OAuth 的工作原理,但在此之前,我们得先了解它的一些术语,因为 OAuth 有非常特别的术语:
- 用户叫 资源所有者(resource owner)
- 应用叫 OAuth 客户端(OAuth client)
- 负责登录的服务器叫 授权服务器(authorization server)
- 存放用户数据的服务器叫 资源服务器(resource server) (有时和授权服务器是同一个)
- 用户选择授权的范围叫 scopes

后续我会尽量用这些术语来表达,因为如果你要继续读更多 OAuth 文档,就得熟悉这些说法。
那我们再用这些新术语来回顾一下刚才的流程。
OAuth 流程,第二遍(术语版)
一个 OAuth 客户端想访问资源服务器上的用户数据(资源所有者的东西)。

它把用户重定向到授权服务器,用户登录、同意授权范围,然后带着授权码重定向回 OAuth 客户端。

然后,客户端再用 _授权码 + Client Secret _去授权服务器换取 Access Token。(等会我们会讨论Client Secret)

和第一遍一样,只是换了专业术语。
我们已经从用户的角度了解了整个流程,现在让我们从开发者的角度看看它是什么样子。
注册新APP

要使用 OAuth,你首先得注册一个新App。举个例子,GitHub 支持 OAuth,如果你想在 GitHub 上创建一个App,就必须先去注册。不同的平台在注册应用时会要求填写的数据可能各不相同,但至少都会要求你提供以下这些基本信息:
- App 名称,因为当用户访问 GitHub 时,GitHub 需要能够说"某某App 正在请求对您的 repos 和 gists 的读取权限"。
- 重定向 URI,我们稍后会讨论它是什么。
GitHub 会给你:
- Client ID,这是一个公开的ID,需要用它来发出请求。
- Client Secret(客户端密钥) ,用来验证请求是否是有效的。
太好了,你已经注册好了自己的 OAuth 应用。假设你的应用是 YNAB,而你的第一个用户想把它连接到 Chase 银行,于是你就要开启一次新的 OAuth 流程......这就是你的第一次真正跑起来的 OAuth 流程!
第一步:你需要把用户重定向到 Chase 的授权服务器的 OAuth 接口,并在 URL 中传递这些参数:

- Client ID:就是我们刚才提到的那个。
- Redirect URI:当用户在 Chase 的操作完成后,Chase 会把他们重定向回来,这个地址一般就是 YNAB 的某个 URL,因为你就是 YNAB 应用。
- Response type :通常是
"code"
,因为我们通常需要的是一个授权码(authorization code),而不是直接拿到 Access Token(这样不安全)。 - Scopes:也就是请求的权限范围。换句话说,你要访问用户的哪些数据?
这些信息足够让授权服务器验证请求,并给用户展示一条提示,比如"YNAB 正在请求访问你的账户数据(只读)"。
那么授权服务器是怎么验证请求的呢?如果 Client ID 无效,请求立刻就会被拒绝。如果 Client ID 有效,还需要检查 redirect URI。为什么呢?因为 Client ID 是公开的,任何人都能拿到 YNAB 的 Client ID,伪造一个 OAuth 流程去访问 Chase,然后再把用户重定向到某个恶意网站,比如 evildude.com。正是因为这个风险,你在注册应用时必须告诉 Chase 有效的 redirect URI 长什么样。这样 Chase 就会只允许跳转回 YNAB.com 的地址,从而避免这种 evildude.com 的攻击场景。
如果一切验证通过,授权服务器就能通过 Client ID 找到应用名称,甚至显示应用图标,然后给用户弹出授权确认界面。
用户勾选想要授权给 YNAB 的账户,然后点击确认。
接着,Chase 会把他们重定向回你提供的 redirect URI,比如说 ynab.com/oauth-callback?authorization_code=xyz
注 :你可能在想 URI 和 URL 有啥区别,因为我这里两个词都在用。其实 URL 是 URI 的一种。URL 就是我们熟悉的网页地址,而 URI 更泛一些,不只是网页。
为什么这里说 redirect URI 而不是 URL?因为移动应用没有传统意义上的 URL,它们会用自定义协议来做,比如
myapp://foobar
。所以,如果你做的是 Web 应用,可以把 URI 理解成 URL;如果你做的是移动端,看到 URI 也能明白你的场景是支持的。
于是用户就被重定向回了 ynab.com/oauth-callback?authorization_code=xyz
。
现在你的应用就拿到了授权码。接下来,你要把这个授权码和 Client Secret 一起发给 Chase 的授权服务器。为什么要带上 Client Secret?因为授权码是在 URL 里的,谁都能看到,也可能有人拿着它去尝试兑换 Access Token。所以必须带上 Client Secret,这样 Chase 的服务器就能验证:"嗯,我确实为这个 Client ID 生成过这个授权码,而且 Client Secret 也对得上,所以这个请求有效。"
然后,Chase 就会返回给你 Access Token。
你会发现,在 OAuth 流程的每一步,设计者都考虑到了潜在的攻击手段,并加上了安全措施。这也是为什么 OAuth 看起来这么复杂的原因之一。
*我有个从事安全行业的朋友告诉我,OAuth 的设计者其实是一路踩坑做出来的,过程中不断被迫打补丁,所以它才变得这么复杂。
另一个让它复杂的原因,是因为用户必须参与其中。所有和用户交互的环节都得放在前端,而前端本质上不安全,因为所有东西都是公开的。真正敏感的环节只能放在后端来处理。
我一直在说前端(frontend)和后端(backend),但在 OAuth 文档里,他们更喜欢用 前端通道(front-channel) 和 后端通道(back-channel) 这两个词。为什么呢?

前端通道(front-channel)和后端通道(back-channel)
在 OAuth 里:
- 前端通道(front-channel) :通过浏览器重定向传递数据,通常是 GET 请求,参数会暴露在 URL 中,谁都能看到。
- 后端通道(back-channel) :应用直接和服务器之间的安全通信,通常是 HTTPS POST,数据不会暴露给用户。
他们不用 frontend/backend (前端/后端)这类词,是因为技术上你完全可以用 JavaScript 发 POST 请求。理论上,你甚至可以在前端直接用 JS 把授权码兑换成 Access Token。
但问题是,你还需要 Client Secret。如果把 Secret 放在前端(JavaScript 里),它就不再是 Secret 了,谁都能拿到。所以在前端场景里,有另一种方法:PKCE(拼作 P-K-C-E,念作"pixie")。
PKCE 的安全性略低于带 Client Secret 的后端流程,但如果你没有后端,那这就是你的推荐方案。所以只要记住:即使没有后端,你也可以用 PKCE 来跑 OAuth。
我可能会在以后的文章中介绍PKCE,它现在甚至被推荐用于标准流程,因为它能防止授权码被拦截。
PS:这里的"我"指原作者:)
移动应用也有同样的问题。除非你的移动端配有一个后端服务器,否则放在 App 软件包里的 Client Secret 很容易被提取出来。所以在移动端同样应该用 PKCE 来获取 Access Token,而不是直接把 Client Secret 放进去。

所以,这里你需要记住两个新词:front-channel 和 back-channel。
到这里,你已经从用户视角和开发者视角都看过了 OAuth 流程,也理解了它为什么复杂但安全。
最后的一些想法
OAuth 的实现方式有很多。我上面讲的是推荐的标准流程,但有人会用另一种方式:直接在重定向里返回 Access Token(这叫 隐式流程 implicit flow),也有人会用 PKCE,甚至有些人搞出不需要用户同意的 OAuth(这是个糟糕的主意)。
另外,OAuth 的 token 不是永久有效的,它们会过期,你需要通过 刷新流程(refresh flow) 来获取新的 token。
还有一点,OAuth 本质上是做授权(authorization)的,但有些场景也把它用在登录上,比如"使用 Google 登录"。这其实是 OAuth 之上的一层标准,叫 OpenID Connect(OIDC) ,它会返回用户信息,而不仅仅是 Access Token。
我之所以特别提这个,是因为你在网上搜 OAuth,会看到很多不同的流程,容易被搞糊涂,会想为什么都不一样。原因就是:OAuth 不是像 HTTP 那样单一明确的协议,它可以有很多不同的形式。
好了,现在你已经可以自己去玩 OAuth 了。祝你好运!
