OAuth 图解指南:从零开始理解授权流程
- Published on
目录

OAuth 最早于 2007 年推出。它的诞生源于 Twitter 的一个需求:Twitter 希望有一种方式允许第三方应用代表用户发布推文。花点时间想象一下,如果今天你要设计这样的功能,你会怎么做?
一种方法是直接向用户索要他们的用户名和密码。所以你创建了一个非官方的 Twitter 客户端,向用户展示一个登录屏幕,上面写着"使用 Twitter 登录"。用户照做了,但实际上他们并不是在登录 Twitter,而是把数据发送给了你这个第三方服务,由你代表他们登录 Twitter。

这种方式有很多问题。即使你信任第三方应用,如果他们没有正确存储你的密码而被人窃取了怎么办?你永远不应该把密码提供给这样的第三方网站。
你可能还会想到另一种方式:API 密钥如何?因为你要访问 Twitter 的 API 来为用户发布数据,而对于 API,你使用 API 密钥。但 API 密钥是通用的。你需要的是一个特定于用户的 API 密钥。

为了解决这些问题,OAuth 应运而生。你将看到它如何解决所有这些问题,但 OAuth 的核心是一个 访问令牌(access token),它有点像特定于用户的 API 密钥。应用获得一个访问令牌,然后就可以用它代表用户执行操作,或访问用户的数据。
OAuth 如何工作

OAuth 可以以多种不同的方式使用,这也是它如此难以理解的原因之一。在本文中,我们将看一个典型的 OAuth 流程。
我要使用的例子是 YNAB。如果你没用过,YNAB 就像是 Mint 的付费版本。你把它连接到银行账户,然后它会从该账户拉取所有交易记录,用非常漂亮的图表展示给你。你可以对支出进行分类,然后它会告诉你,比如,嘿,你在杂货上花得太多了。它帮助你管理财务。所以,我想使用 YNAB,并想把它连接到 Chase 银行,但我不想给它我的 Chase 密码。所以我要使用 OAuth。
让我们先看看流程,然后再理解发生了什么。实际上我们要看两遍这个流程,因为我认为你需要至少看两遍 OAuth 流程才能理解发生了什么。
OAuth 流程,第一遍
所以首先,我在 YNAB 上,想把 Chase 作为数据源连接。OAuth 流程如下:
YNAB 将我重定向到 Chase。
在 Chase,我用用户名和密码登录。
Chase 向我显示一个屏幕,说"YNAB 想要连接到 Chase。选择你想授予 YNAB 访问权限的账户"。它会显示我所有账户的列表。假设我只选择我的支票账户,授予 YNAB 对该账户的读取权限,然后点击确定。
从 Chase,我被重定向回 YNAB,现在神奇地,YNAB 已经连接到 Chase 了。

这是从用户角度看到的体验。但这背后发生了什么?YNAB 是如何以某种方式获得我在 Chase 上的数据访问权限的?
最终目标是获得访问令牌
记住,OAuth 的最终目标是让 YNAB 获得一个访问令牌,这样它就可以从 Chase 访问我的数据。不知何故,当我经历这个流程时,YNAB 获得了访问令牌。我先告诉你它如何获得访问令牌,然后再详细介绍发生了什么。
关于安全性的说明
Chase 如何将访问令牌给到 YNAB?当你从 Chase 重定向回 YNAB 时,Chase 可以直接在 URL 中添加访问令牌。它可以把你重定向到这样的 URL:
https://www.ynab.com/redirect?access_token=123
然后 YNAB 就能获得访问令牌了。
这是个坏主意!!
访问令牌应该是保密的,但 URL 可能会出现在你的浏览器历史记录或某些服务器日志中,这样任何人都很容易看到你的访问令牌。
所以 Chase 在技术上可以在 URL 中用访问令牌把你重定向回 YNAB,然后 YNAB 就有了访问令牌。OAuth 流程结束。但我们不这样做,因为在 URL 中发送访问令牌是不安全的。
当你从 Chase 重定向回 YNAB 时,Chase 在 URL 中发送给 YNAB 的是一个授权码(authorization code)。
授权码不是访问令牌!Chase 向 YNAB 发送授权码,YNAB 用授权码交换访问令牌。它通过向 Chase 发起后端请求来完成,是一个通过 HTTPS 的后端 POST 请求,这意味着没有人可以看到访问令牌。

然后 YNAB 就有了访问令牌。OAuth 流程结束。OAuth 成功。
OAuth 的两个部分

让我们谈谈我们刚才看到的内容。在高层次上,OAuth 流程有两个部分。第一个是用户同意流程,这是你(用户)登录并选择授予访问权限的地方。这是 OAuth 的关键部分,因为在 OAuth 中,我们总是希望用户积极参与并掌控。
另一部分是授权码流程。这是 YNAB 实际获得这个访问令牌的流程。
让我们更详细地讨论这到底是如何工作的。让我们也谈谈一些术语,因为 OAuth 有非常特定的术语。
- 我们不说用户,而是说 资源所有者(resource owner)。
- 我们不说应用,而是说 OAuth 客户端(OAuth client) 或 OAuth 应用(OAuth app)。
- 你登录的服务器称为 授权服务器(authorization server)。你从中获取用户数据的服务器称为 资源服务器(resource server)(这可能与授权服务器相同)。
- 在授权服务器上,当用户选择允许什么时,这些被称为 作用域(scopes)。

我会尽量使用这些术语,因为如果你要阅读更多 OAuth 文档,你需要熟悉它。
所以让我们再次以新术语高层次地看一下这个流程。
OAuth 流程,第二遍
你有 OAuth 客户端。OAuth 客户端想要访问资源服务器上的数据,而数据属于资源所有者。

为此,OAuth 客户端重定向到授权服务器。用户登录,用户同意 作用域(这个令牌被允许访问什么),然后用户被重定向回 OAuth 客户端,URL 中带有授权码。

在后端,OAuth 客户端将授权码和客户端密钥(我们稍后会谈到客户端密钥)发送到授权服务器,授权服务器响应访问令牌。

这是完全相同的流程,但使用了我们刚刚讨论的新术语。现在让我们谈谈具体细节。我们已经从用户的角度看了这个流程,让我们从开发者的角度来看。
注册新应用

要使用 OAuth,你首先需要注册一个新应用。例如,GitHub 提供 OAuth。如果你想为 GitHub 创建一个新应用,你首先要注册它。不同的服务在应用注册时需要不同类型的数据,但每个服务至少需要:
应用名称,因为当用户访问 GitHub 时,例如,GitHub 需要能够说"Amazon Web Services 正在请求对你的仓库和 gists 的读取权限"
重定向 URI。我们稍后会讨论这是什么。
GitHub 会响应:
客户端 ID(Client ID)。这是一个公开 ID,你将用它来发起请求
客户端密钥(Client Secret)。你将用它来验证你的请求。
太棒了,你已经注册了你的 OAuth 应用。假设你的应用是 YNAB,你的一个用户想要连接到 Chase。所以你开始一个新的 OAuth 流程...你的第一个!
第一步:你将把他们重定向到 Chase 授权服务器的 OAuth 端点,在 URL 中传递这些参数:

客户端 ID,我们刚才谈到的。
重定向 URI。一旦用户在 Chase 上完成,Chase 会把他们重定向到这里。这将是一个 YNAB 的 URL,因为你是 YNAB 应用。
响应类型(Response type),通常是"code",因为我们通常希望获得授权码,而不是访问令牌,后者不太安全。
作用域(Scopes)。我们请求什么作用域?即我们想要访问什么用户数据?
这足够授权服务器验证请求并向用户显示类似"YNAB 正在请求对你账户的读取权限"的消息。
授权服务器如何验证请求?如果客户端 ID 无效,请求立即无效。如果客户端 ID 有效,授权服务器需要检查重定向 URI。基本上,由于客户端 ID 是公开的,任何人都可以获取 YNAB 客户端 ID,创建自己的 OAuth 流程来访问 Chase,但然后将用户返回到,比如说,evildude.com。但这就是为什么当你注册应用时,你必须告诉 Chase 有效的重定向 URI 是什么样的。此时,你会告诉 Chase 只有 YNAB.com 的 URI 是有效的,从而防止这种 evildude.com 场景。
如果一切有效,授权服务器可以使用客户端 ID 获取应用名称,也许还有应用图标,然后显示用户同意屏幕。
用户会点击他们想要授予 YNAB 访问权限的账户,然后点击确定。
Chase 会将他们重定向回你提供的重定向 URI,比如 ynab.com/oauth-callback?authorization_code=xyz。
旁注:你可能想知道,URI 和 URL 有什么区别?因为我有点同时使用两者。URL 是我们熟知和喜爱的任何网站 URL。URI 更通用。URL 是 URI 的一种类型,但还有许多其他类型的 URI。

我说重定向 URI 而不是重定向 URL 的原因是,移动应用不会有 URL。它们只有 URI,这是一个它们自己编造的协议,可能看起来像
myapp://foobar。所以如果你只做 Web 工作,每当你读到 URI 时,你可以把它读作 URL。如果你做移动开发,你可以读 URI 并知道,是的,你的用例也被支持。
所以用户被重定向回 ynab.com/oauth-callback?authorization_code=xyz,现在你的应用有了授权码。你将该授权码连同你的客户端密钥一起发送到 Chase 授权服务器。为什么要包含客户端密钥?因为授权码在 URL 中。所以任何人都可以看到它,任何人都可以尝试用它交换访问令牌。这就是为什么我们需要发送客户端密钥,这样 Chase 的服务器就可以说"哦是的,我记得我为这个客户端 ID 生成了这个代码,客户端密钥匹配。这是一个有效的请求。"
然后它返回访问令牌。注意在 OAuth 流程的每一步中,他们都考虑了有人如何利用该流程,并添加了保障措施*。这是它如此复杂的一个重要原因。
*我从安全领域的一位朋友那里得知,OAuth 设计者以艰难的方式学到了很多教训,这是它如此复杂的另一个原因:因为它必须被反复修补。
另一个重要原因是我们希望用户参与其中。这使得它变得复杂,因为所有用户相关的内容都必须在前端,这是不安全的,因为任何人都可以看到它。然后所有安全的内容都必须在后端。
我一直在说前端和后端,但在 OAuth 文档中,他们说的是前端通道(front-channel)和后端通道(back-channel)。让我们谈谈为什么。

前端通道和后端通道
所以,OAuth 不使用前端和后端这些术语,它使用前端通道和后端通道。前端通道意味着 GET 请求,任何人都可以看到 URL 中的参数,后端通道意味着 POST 请求,其中数据是加密的(作为 POST 主体的一部分)。OAuth 不使用前端或后端的原因是,你可以使用 JavaScript 发起 POST 请求!所以,理论上,你可以在前端用 JavaScript 直接通过发起 POST fetch 请求来用授权码交换访问令牌。
现在,这有一个大问题,那就是你还需要客户端密钥来发起该请求。当然,一旦密钥在前端并且可以在 JavaScript 中访问,它就不再是秘密了。任何人都可以访问它。所以,不使用客户端密钥,有另一种方法叫做 PKCE,拼写为 P-K-C-E,发音为"pixie"(真的)。它不如在后端使用客户端密钥安全,但如果后端不是你的选项,你可以使用 PKCE。所以只要知道如果你有一个没有后端的应用,你仍然可以做 OAuth。
我可能会在未来的文章中介绍 PKCE,因为现在也建议在标准流程中使用它,因为它有助于防止授权码拦截。
移动应用也有同样的问题。除非你有一个有后端组件的移动应用,比如某处的后端服务器,否则如果你把客户端密钥放在移动应用中,任何人都可以获得它,因为有大量工具可以从移动应用中提取字符串。所以,不要在应用中包含客户端密钥,你应该再次使用 PKCE 来获得访问令牌。
所以这是另外两个需要了解的术语:前端通道和后端通道。

此时,你已经从用户的角度和开发者的角度看到了 OAuth 流程是什么样的,你也看到了使其安全的组件。
一些最后的想法
我想提到的最后一件事是 OAuth 可以有很多不同的形式。我在上面介绍了主要推荐的 OAuth 流程,但有些人可能会通过在重定向中传回访问令牌而不是授权令牌来做 OAuth(这样做被称为"隐式流程")。有些人可能使用 PKCE 来做。甚至有一种方法可以在没有用户同意部分的情况下做 OAuth,但这真的不推荐。
我们没有涵盖的 OAuth 的另一部分是令牌会过期,你需要刷新它们。这通过刷新流程发生。此外,OAuth 完全是关于授权的,但一些工作流使用 OAuth 来登录,比如当你使用"使用 Google 登录"功能时。这使用 OpenID Connect,或 OIDC,它是 OAuth 之上的一层,还返回用户数据而不仅仅是访问令牌。我在这里提到这一点,因为当你在网上查找 OAuth 时,你会看到很多不同的流程,你可能会对为什么它们都不同感到困惑。原因是,OAuth 不像 HTTP 那样直接,OAuth 可以有很多不同的形式。
现在你已经准备好出去做你自己的 OAuth 了。祝你好运!
总结
OAuth 是一个强大但复杂的授权标准。它的复杂性主要来自于:
- 安全考虑:每一步都考虑了潜在的安全风险并添加了保障措施
- 用户参与:需要用户主动同意,增加了前后端交互的复杂性
- 灵活性:支持多种不同的流程和使用场景
理解 OAuth 的关键是:
- 访问令牌是最终目标
- 授权码用于安全地获取访问令牌
- 用户同意是核心环节
- 客户端密钥用于验证请求的合法性
希望这篇图解指南能帮助你更好地理解 OAuth!