使用 OAuth 2.0 和 OpenID Connect 保护基于云的应用程序
一份完整的指南,帮助你使用 OAuth 2.0 和 OpenID Connect 保护云应用程序,并在身份验证和授权过程中提供极佳的用户体验。
介绍
基于云的应用程序是当今的趋势。虽然应用程序的类型各异(网页、移动端、桌面端等),但它们都有一个提供存储、计算和数据库等服务的云端后端。大多数时候,这些应用程序需要验证用户身份,并授权他们访问某些资源。
虽然可以使用自制的身份验证和授权机制,但在开发云应用程序时,安全性已成为一个主要关注点。值得庆幸的是,我们的行业已提供经过实战检验的标准,如 OAuth 2.0 和 OpenID Connect,帮助我们实施安全的身份验证和授权。
本篇文章有以下假设:
- 你对应用程序开发(网页、移动端或其他类型)有基本了解。
- 你听说过身份验证和授权的概念。
- 你听说过 OAuth 2.0 和 OpenID Connect。
没错,对于 2 和 3,"听说过"就足够了。本文会使用实际案例来解释概念,并通过图示来描绘流程。让我们开始吧!
OAuth 2.0 与 OpenID Connect
如果你已经熟悉 OAuth 2.0 和 OpenID Connect,你也可以继续阅读,因为我们将在本节介绍一些实际案例;如果你是新手,也可以继续,因为我们会用简单的方法介绍它们。
OAuth 2.0
OAuth 2.0 是一个授权框架,它允许应用程序在用户或应用程序本人的代理下获取对另一个应用程序的保护资源的有限访问。大多数流行的服务如 Google、Facebook 和 GitHub 使用 OAuth 2.0 进行社交登录(例如,"使用 Google 登录")。
例如,你有一个名为 MyApp 的网络应用程序,想要访问用户的 Google Drive。与其要求用户分享他们的 Google Drive 凭证,MyApp 可以使用 OAuth 2.0 请求代表用户访问 Google Drive。下面是一个简化的流程:
在这个流程中,MyApp 从未看到用户的 Google Drive 凭证。相反,它从 Google 获得一个 访问令牌,允许它代表用户访问 Google Drive。
在 OAuth 2.0 的术语中,MyApp 是 客户端,简单起见,Google 既是 授权服务器又是 资源服务器。在实际中,我们通常有独立的授权服务器和资源服务器,以提供单点登录(SSO)体验。例如,Google 是授权服务器,它可能有多个资源服务器,如 Google Drive、Gmail 和 YouTube。
请注意,实际的授权流程比这更复杂。OAuth 2.0 有不同的授权类型、范围以及其他概念,你应该对此有所了解。现在让我们把这些放在一边,进入 OpenID Connect。
OpenID Connect (OIDC)
OAuth 2.0 在授权方面很出色,但你可能注意到它没有一种识别用户的方法(即,身份验证)。OpenID Connect 是一个在 OAuth 2.0 之上增加身份验证功能的身份层。
在上述示例中,MyApp 需要在启动授权流程之前知道用户是谁。注意这里涉及两个用户:MyApp 的用户和 Google Drive 的用户。在这种情况下,MyApp 需要识别其自身应用程序的用户。
让我们来看一个简单的例子,假设用户可以通过用户名和密码登录 MyApp:
由于我们正在认证自己应用程序的用户,通常不需要像在 OAuth 2.0 流程中那样请求权限。同时,我们需要一些可以识别用户的东西。OpenID Connect 引入了 ID 令牌 和 用户信息端点 的概念来帮助我们。
你可能注意到 身份提供商 (IdP) 是流程中的一个新的独立参与者。它与 OAuth 2.0 中的 授权服务器 相同,但为了更清晰,我们使用 IdP 这个术语,表明它负责用户身份验证和身份管理。
当你的业务发展时,你可能会有多个共享相同用户数据库的应用程序。就像 OAuth 2.0,OpenID Connect 允许你拥有一个单一的授权服务器来为多个应用程序验证用户身份。如果用户已经登录到一个应用程序,当另一个应用程序将他们重定向到 IdP 时,他们无需再次输入凭证。这个流程可以自动完成,而无需用户交互。这被称为单点登录(SSO)。
同样,这也是一个高度简化的流程,相关细节远不止如此。目前,我们转到下一节,防止信息过载。
应用程序类型
在上一节中,我们使用的是 Web 应用程序作为示例,但世界比这更加多样化。对身份提供商来说,你使用的确切编程语言、框架或平台并不重要。实际上,值得注意的一点区别是应用程序是 公共客户端 还是 私有(可信)客户端:
- 公共客户端:无法保密其凭证的客户端,意味着资源所有者(用户)可以访问它们。例如,运行在浏览器中的 Web 应用程序(例如单页应用程序)。
- 私有客户端:能够保密其凭证且不暴露给(资源所有者)用户的客户端。例如,运行在服务器上的 Web 应用程序(如服务器端 Web 应用程序)或 API 服务。
考虑到这些,让我们看看 OAuth 2.0 和 OpenID Connect 如何应用于不不同的应用程序类型。
在本文的上下文中,"应用程序"和"客户端"可以互换使用。
运行在服务器上的 Web 应用程序
应用程序运行在服务器上,为用户提供 HTML 页面。许多流行的 Web 框架如 Express.js、Django 和 Ruby on Rails 属于这一类别;前端后端开发(BFF)框架如 Next.js 和 Nuxt.js 也包括在内。这些应用程序有以下特征:
- 由于服务器仅允许私有访问(公共用户无法看到服务器的代码或凭证),它被视为私有客户端。
- 整体用户身份验证流程与"OpenID Connect"一节中讨论的相同。
- 应用程序可以使用身份提供商(即 OpenID Connect 提供商)颁发的 ID 令牌来识别用户并显示用户特定的内容。
- 为了保证应用程序的安全,应用程序通常使用 授权码流程 进行用户身份验证并获取令牌。
同时,该应用程序可能需要访问微服务架构中的其他内部 API 服务;或者它是一个需要对应用程 序不同部分进行访问控制的单体应用程序。我们将在"保护你的 API"部分讨论这个问题。
单页应用程序(SPAs)
应用程序在用户的浏览器中运行,通过 API 与服务器通信。React、Angular 和 Vue.js 是构建 SPAs 的流行框架。这些应用程序有以下特征:
- 由于应用程序的代码对公众可见,它被视为公共客户端。
- 整体用户身份验证流程与"OpenID Connect"部分中讨论的相同。
- 应用程序可以使用身份提供商(即 OpenID Connect 提供商)颁发的 ID 令牌来识别用户并显示用户特定的内容。
- 为了保证应用程序的安全,应用程序通常使用 带 PKCE(校验码验证)的授权码流程 进行用户身份验证并获取令牌。
通常,SPA 需要访问其他 API 服务进行数据获取和更新。我们将在"保护你的 API"部分讨论这个问题。
移动应用程序
应用程序在移动设备上运行(iOS、Android 等),通过 API 与服务器通信。在大多数情况下,这些应用程序与 SPA 具有相同的特征。
机器对机器(M2M)应用程序
机器对机器 应用程序是运行在服务器(机器)上的 客户端,与其他服务器通信。这些应用程序有以下特征:
- 像在服务器上运行的 Web 应用程序一样,M2M 应用程序是私有客户端。
- 应用程序可能不需要识别用户;而是需要验证自身以访问其他服务。
- 应用程序可以使用身份提供商(即 OAuth 2.0 提供商)颁发的访问令牌来访问其他服务。
- 为了保证应用程序的安全,应用程序通常使用 客户端凭证流程 获取访问令牌。
在访问其他服务时,应用程序可能需要在请求头中提供访问令牌。我们将在"保护你的 API"部分讨论这个问题。
运行在物联网设备上的应用程序
应用程序运行在物联网设备上(例如智能家居设备、可穿戴设备等),通过 API 与服务器通信。这些应用程序有以下特征:
- 根据设备的能力,它可以是公共或私有客户端。
- 总体身份验证流程可能与"OpenID Connect"部分讨论的不同,视设备能力而定。例如,某些设备可能没有屏幕让用户输入其凭证。
- 如果设备不需要识別用户,则可能不需要使用 ID 令牌或用户信息端点;相反,它可以作为机器对机器(M2M)应用程序来处理。
- 为了保证应用程序的安全,应用程序可能使用 带 PKCE(校验码验证)的授权码流程 进行用户身份验证并获取令牌,或使用 客户端凭证流程 获取访问令牌。
在与服务器通信时,设备可能需要在请求头中提供访问令牌。我们将在"保护你的 API"部分讨论这一问题。
保护你的 API
借助 OpenID Connect,可以通过 ID 令牌 或 用户信息端点 来识别用户和获取用户特定的数据。这一过程被称为身份验证。但是,你可能不希望所有资源对所有经过身份验证的用户开放,例如,仅管理员可以访问用户管理页面。
这就是授权的用武之地。记住 OAuth 2.0 是一个授权框架,而 OpenID Connect 是一个基于 OAuth 2.0 的身份层;这意味着当 OpenID Connect 已经启动时,你也可以使用 OAuth 2.0。
让我们回忆一下在"OAuth 2.0"部分中用过的例子:MyApp 想要访问用户的 Google Drive。让 MyApp 访问用户 Google Drive 中的所有文件是不切实际的。相反,MyApp 应明确表明它想要访问的内容(例如,特定文件夹中文件的只读访问)。在 OAuth 2.0 中,这被称为 范围。
你可能会看到,在 OAuth 2.0 的背景下,"权限"这个术语有时与"范围"可以交换使用,因为对于非技术用户来说,"范围"有时模糊。
当用户同意授予 MyApp 访问权时,授权服务器会发出具有请求范围的访问令牌。然后,该访问令牌被送到资源服务器(Google Drive)来访问用户的文件。
当然,我们可以将 Google Drive 替换为我们自己的 API 服务。例如,MyApp 需要访问 OrderService 来获取用户的订单历史记录。这一次,由于用户身份验证已由身份提供商完成,并且 MyApp 和 OrderService 都在我们的控制范围内,我们可以跳过请求用户授权;MyApp 可以直接向 OrderService 发送带有身份提供商颁发的访问令牌的请求。
访问令牌可能包含一个 read:order
范围 ,以表明用户可以查看他们的订单历史。
现在,让我们假设用户在浏览器中错误地输入了管理员页面的 URL。由于用户不是管理员,访问令牌中没有 admin
范围。OrderService 将拒绝请求并返回错误消息。
在这种情况下,OrderService 可能返回一个 403 Forbidden 状态码,以表明用户无权访问管理员页面。
对于机器对机器(M2M)应用程序,过程不涉及用户。应用程序可以直接从身份提供商请求访问令牌并用它们访问其他服务。相同的概念适用:访问令牌包含访问资源所需的范围。
授权设计
我们可以看到两个重要的事项需要在设计 授权 以保护你的 API 服务时考虑:
- 范围:定义客户端可以访问什么。范围可以是精细的(例如,
read:order
,write:order
),也可以是更一般的(例如,order
),这取决于你的需求。 - 访问控制:定义谁可以拥有特定的范围。例如,只有管理员可以拥有
admin
范围。
关于 访问控制,一些流行的方法有:
- 基于角色的访问控制 (RBAC):为用户分配角色,并定义哪些角色可以访问哪些资源。例如,管理员角色可以访问管理员页面。
- 基于属性的访问控制 (ABAC):基于属性定义策略(例如,用户的部门、位置等),并根据这些属性进行访问控制决策。例如,"工程"部门的用户可以访问工程页面。
值得一提的是,对于这两种方法,验证访问控制的标准方法是检查访问令牌的范围,而不是角色或属性。角色和属性可能非常动态,而范围更为静态,使其更易于管理。
有关访问控制的详细信息,你可以参阅 RBAC 和 ABAC:你应该知道的访问控制模型。
访问令牌
虽然我们多次提到了 "访问令牌" 一词,但我们还没有讨论如何获取一个。在 OAuth 2.0 中,访问令牌由授权服务器(身份提供商)在成功的授权流程之后颁发。
让我们更深入地看看 Google Drive 的示例,并假设我们使用的是授权码流程:
在访问令牌颁发流程中有一些重要步骤:
- 第 2 步 (重定向到 Google):MyApp 使用一个 授权请求 将用户重定向到 Google。通常,这个请求包含以下信息:
- 哪个客户端(MyApp)正在发起请求
- MyApp 请求了哪些范围
- 在授权完成后 Google 应将用户重定向到哪里
- 第 5 步 (请求访问 Google Drive 的权限):Google 请求用户授权 MyApp。用户可以选择授权或拒绝。这一步称为 同意。
- 第 7 步 (重定向到 MyApp 并携带授权数据):这个步骤在图中被新引入。Google 返回一个一次性 授权码 给 MyApp,而不是直接返回访问令牌,以便更安全的交换。此代码用于获取访问令牌。
- 第 8 步 (使用授权码交换访问令牌):这也是一个新步骤。MyApp 向 Google 发送授权码以交换访问令牌。作为身份提供者,Google 将汇总请求的上下文,并决定是否颁发访问令牌:
- 客户端(MyApp)确实是其声称的身份
- 用户已授予客户端访问权限
- 用户是其声称的身份
- 用户具有必要的范围
- 授权码有效且未过期
以上示例假设授权服务器(身份提供者)和资源服务器相同(Google)。如果它们是独立的,以 MyApp 和 OrderService 为例,流程将是这样的:
在这个流程中,授权服务器(IdP)向 MyApp 颁发了 ID 令牌和访问令牌(第 8 步)。ID 令牌用于识别用户,访问令牌用于访问其他服务,如 OrderService。由于 MyApp 和 OrderService 都是一方服务,它们通常不要求用户授权;相反,它们依靠身份提供者中的访问控制来确定用户是否可以访问资源(即,访问令牌是否包含必要的范围)。
最后,让我们看看在机器对机器(M2M)应用程序中如何使用访问令牌。由于流程中不涉及用户且应用程序是可信的,它可以直接从身份提供者请求访问令牌:
在这里仍然可以应用访问控制。对于 OrderService 来说,谁是用户或哪个应用程序请求数据并不重要;它只关心访问令牌及其包含的范围。
访问令牌通常编码为 JSON Web Tokens (JWT)。更多关于 JWT 的信息,你可以参考 What is JSON Web Token (JWT)?。
资源指示器
OAuth 2.0 引入了用于访问控制的范围概念。然而,你可能很快意识到范围还不够:
- OpenID Connect 定义了一组标准范围,如
openid
、offline_access
和profile
。将这些标准范围与自定义范围混合在一起可能会令人困惑。 - 你可能有多个 API 服务共享相同的范围名称,但含义不同。
一种常见的解决方案是为范围名称添加后缀(或前缀)以指示资源(API 服务)。例如,read:order
和 read:product
比 read
和 read
更清晰。OAuth 2.0 扩展 RFC 8707 引入了一个新参数 resource
,用于指示客户端想要访问的资源服务器。
实际上,API 服务通常由 URL 定义,所以使用 URL 作为 资源指示器更合适。例如,OrderService API 可以表示为:
如你所见,该参数在授权请求和访问令牌中带来了使用实际资源 URL 的便利。值得一提的是,RFC 8707 并未被所有的身份提供者实现。你应查看身份提供者的文档以确认其是否支持 resource
参数(Logto 支持它)。
简而言之,resource
参数可以在授权请求和访问令牌中使用,以指示客户端想要访问的资源。
对资源指示器的访问性没有限制,即资源指示器不需要是一个真正指向 API 服务的 URL。因此 "资源指示器" 这个名称恰当地反映了它在授权过程中的角色。你可以定义虚拟 URL 来表示你想保护的资源。例如,你可以在你的单体应用程序中定义一个虚拟 URL
https://api.example.com/admin
,来表示只有管理员可以访问的资源。
综合运用
迄今为止,我们已经讨论了 OAuth 2.0 和 OpenID Connect 的基础知识,以及如何在不同的应用程序类型和场景中使用它们。虽然我们分别讨论了它们,但你可以根据业务需要将它们结合起来。整体架构可以像这样简单:
或者稍微复杂一点:
随着你的应用程序的发展,你会发现身份提供者(IdP)在架构中扮演了一个关键角色;但它与业务目标直接无关。虽然将其交给可靠的供应商是个好主意,但我们需要明智地选择身份提供者。一个好的身份提供者可以大大简化流程,减少开发工作量,并让你免于潜在的安全隐患。
结束语
对于现代云应用程序,身份提供者(或授权服务器)是用户 身份验证、身份管理 和 访问控制 的核心位置。虽然我们在这篇文章中讨论了很多概念,但在实施此类系统时仍有许多细微差别需考虑。 如果你对更深入的内容感兴趣,可以浏览我们的博客以获取更深入的文章。