使用 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 是一个授权框架,它允许应用程序在用户或应用程序本人的代理下获取对另一个应用程序的保护资源的有限访问。大多数流行的服务如 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。
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 应用程序作为示例,但世界比这更加多样化。对身份提供商来说,你使用的确切编程语言、框架或平台并不重要。实际上,值得注意的一点区别是应用程序是 公共客户端 还是 私有(可信)客户端:
考虑到这些,让我们看看 OAuth 2.0 和 OpenID Connect 如何应用于不不同的应用程序类型。
在本文的上下文中,"应用程序"和"客户端"可以互换使用。
应用程序运行在服务器上,为用户提供 HTML 页面。许多流行的 Web 框架如 Express.js、Django 和 Ruby on Rails 属于这一类别;前端后端开发(BFF)框架如 Next.js 和 Nuxt.js 也包括在内。这些应用程序有以下特征:
同时,该应用程序可能需要访问微服务架构中的其他内部 API 服务;或者它是一个需要对应用程序不同部分进行访问控制的单体应用程序。我们将在"保护你的 API"部分讨论这个问题。
应用程序在用户的浏览器中运行,通过 API 与服务器通信。React、Angular 和 Vue.js 是构建 SPAs 的流行框架。这些应用程序有以下特征:
通常,SPA 需要访问其他 API 服务进行数据获取和更新。我们将在"保护你的 API"部分讨论这个问题。
应用程序在移动设备上运行(iOS、Android 等),通过 API 与服务器通信。在大多数情况下,这些应用程序与 SPA 具有相同的特征。
机器对机器 应用程序是运行在服务器(机器)上的 客户端,与其他服务器通信。这些应用程序有以下特征:
在访问其他服务时,应用程序可能需要在请求头中提供访问令牌。我们将在"保护你的 API"部分讨论这个问题。
应用程序运行在物联网设备上(例如智能家居设备、可穿戴设备等),通过 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:你应该知道的访问控制模型。
虽然我们多次提到了 "访问令牌" 一词,但我们还没有讨论如何获取一个。在 OAuth 2.0 中,访问令牌由授权服务器(身份提供商)在成功的授权流程之后颁发。
让我们更深入地看看 Google Drive 的示例,并假设我们使用的是授权码流程:
在访问令牌颁发流程中有一些重要步骤:
以上示例假设授权服务器(身份提供者)和资源服务器相同(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、offline_access 和 profile。将这些标准范围与自定义范围混合在一起可能会令人困惑。一种常见的解决方案是为范围名称添加后缀(或前缀)以指示资源(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)在架构中扮演了一个关键角色;但它与业务目标直接无关。虽然将其交给可靠的供应商是个好主意,但我们需要明智地选择身份提供者。一个好的身份提供者可以大大简化流程,减少开发工作量,并让你免于潜在的安全隐患。
对于现代云应用程序,身份提供者(或授权服务器)是用户 身份验证、身份管理 和 访问控制 的核心位置。虽然我们在这篇文章中讨论了很多概念,但在实施此类系统时仍有许多细微差别需考虑。 如果你对更深入的内容感兴趣,可以浏览我们的博客以获取更深入的文章。