13.OAuth 2和OpenID Connect是什么¶
本章内容包括:
- 访问令牌的目的
- 在 OAuth 2 系统中令牌的颁发与校验方式
- OAuth 2/OpenID Connect 系统中的各方角色
假设你在一家大型机构工作,日常使用多种工具:缺陷跟踪应用、工作记录应用、时间登记应用等等。在每一个应用中,你都需要进行认证才能使用。你是否会为这些应用分别使用不同的凭据?当然,这样做是可行的,但对用户(也就是你)来说会非常繁琐,也会让你所使用的各类应用的用途变得更加复杂。
对你来说,复杂性在于需要记住凭据,并在你使用的每个应用中多次登录。对这些应用而言,复杂性在于它们还需实现保存凭据并保护凭据与实际认证的能力。
那有没有办法把保存凭据和认证的职责交给一个单独的应用来处理呢?这样用户只需登录一次,就能在所有应用之间无缝切换,无需重复验证。有这样的解决方案吗?有的。你可以基于 OAuth 2 规范来实现认证。甚至我们可以走得更远。一个面向公众用户的应用(即面向组织外部用户——为全世界打造的应用)也可能需要认证功能。虽然该应用可以自行实现这些功能,但它面临一些挑战:
- 在该应用里实现认证需要更多的工作和投入;
- 用户需要为这个应用创建专门的凭据;
- 有时用户不愿意为他们使用的某个小应用再去创建特殊的凭据。
你有没有想过让这个应用的用户使用已有的账户凭据登录?比如,是否可以允许你的应用用户使用 Facebook、GitHub、Twitter 或 Google 账号登录?你可能会看到这种方式已经非常普遍。许多应用都会让用户选择通过这些社交平台之一来注册和使用。这样一来,应用就让用户用已有的凭据进行身份验证,而不需要你在应用内实现额外的认证功能。这种方式能够:
- 降低成本(例如,无需在应用中实现和维护认证机制)
- 避免用户信任问题(例如,不用再让用户注册新账号并维护一套新的凭据)
- 帮助用户减少凭据的数量
OAuth 2 是一项规范,用于指导如何在系统中划分认证职责。通过这种方式,多个应用可以共用一个实现了认证功能的应用,帮助用户更快地完成认证、让他们的信息更安全,同时降低各个应用的开发成本。
我们先从13.1节开始,我将在其中介绍在基于OAuth 2规范构建认证和授权的系统中涉及的主要角色。在13.1节,你将了解OAuth 2系统中各个角色的职责,比如用户、客户端、授权服务器和资源服务器。13.2节讨论令牌的相关内容。令牌就像应用程序的访问密钥,你将了解可以使用哪些类型的令牌以及在什么场景下使用哪种类型最合适。13.3节回顾了令牌签发的几种关键方式(我们将在「实现OAuth 2授权服务器」实现并测试这些方式)。本章最后是13.4节,我们将在其中梳理在实现OAuth 2时需要注意的潜在问题。
在我们开始之前,我想先说明一下:本章我会以比较简明的方式,概括你需要掌握的核心内容,以便更好地理解「实现OAuth 2授权服务器」到「实现OAuth2客户端」的讨论。我并没有打算在这一章就让你成为 OAuth 2 和 OpenID Connect 的专家——这两者本身都相当复杂,早已有其他作者为它们撰写了整本专著。如果你想进一步深入学习这个领域,我推荐 Justin Richer 和 Antonio Sanso 合著的《OAuth 2 in Action》(Manning,2017)以及 Prabath Siriwardena 的《OpenID Connect in Action》(Manning,2023)。
OAuth 2和OpenID Connect的整体概览¶
假设你需要去一家大公司参加面试,并被邀请前往总部进行面对面交流。但并不是任何人都可以进入公司的办公区域,访客需要遵循一套既定的流程。
要进楼参加讨论,首先需要到前台出示身份证明。确认身份后,前台会发给你一张门禁卡,用来打开特定的门。有些电梯你也可能无法使用,只能乘坐特定的那几部(见图13.1)。
进入这栋楼参与讨论的过程,就像 OAuth 2 中的身份验证和授权机制。你是需要完成特定操作(去某个会议室参加讨论)的用户。为此,你在前台(授权服务器)出示凭证(ID)进行身份验证。一旦确认了你的身份,就会拿到一张通行卡(令牌)。但这张卡只能用于进入特定区域(比如电梯或指定的门),并且有效期很短。讨论结束后,你需要把卡归还给前台。
本节我们将探讨 OAuth 2 系统中相互交织的角色,并解释它如何类似于去某个组织总部面试。我们还会介绍 OAuth 2 作为规范的含义,以及 OpenID Connect(协议)与 OAuth 2(其所依赖的规范)之间的区别。在深入第14至16章的实现之前,我认为充分理解这种认证与授权方式背后的原理至关重要。
首先,让我们了解 OAuth 2 系统中有哪些角色。图 13.2 展示了 OAuth 2 系统的主要参与者。这里的“参与者”指的是在系统功能中扮演角色的任何实体。在 OAuth 2 系统中,您会看到以下参与者:
用户——使用应用的个人。用户通常通过一个前端应用与系统交互,我们称之为客户端。如第 13.3.3 节所讨论的那样,用户并不总是存在于 OAuth 2 系统中,后文您将了解到客户端凭证授权类型。客户端——调用后端、需要进行认证和授权的应用。客户端可以是 Web 应用、移动应用,甚至是桌面应用或独立的后端服务。当客户端是后端服务时,系统通常没有用户。资源服务器——负责授权并响应来自一个或多个客户端应用请求的后端应用。授权服务器——实现认证并安全存储凭据的应用程序。
现在我们来讲讲认证与授权到底是怎么进行的。步骤很简单:
- 用户在客户端应用上执行某个用例;
- 客户端应用获得授权,得以调用资源服务器来处理用户的请求;
- 为了获授权,客户端首先向授权服务器请求一个令牌(称为访问令牌)。这个令牌只是一些特定的信息,用于证明授权服务器已正确识别客户端;
- 客户端在向后端(也就是资源服务器)发起请求时,使用授权服务器颁发的令牌来完成授权。
图 13.3 直观地展示了流程的一部分。图中的编号步骤代表如下内容:
- 用户尝试通过客户端应用执行特定用例。
- 客户端应用知道在拥有可以获得授权的令牌之前无法调用后端。于是客户端向授权服务器请求访问令牌。
- 授权服务器在收到客户端请求后颁发令牌并发送给客户端。
- 客户端使用该令牌向其后端(资源服务器)发送请求。
- 资源服务器对客户端请求进行授权。若授权成功,资源服务器执行请求并回复。
- 客户端将结果呈现给用户。
那么授权服务器发出的这个令牌到底是什么呢?令牌可以是任何一段数据(通常是一串字符),用于证明客户端(和/或用户)已被授权服务器识别。令牌还是一种获取用户和客户端更多信息的方式。如果需要,后端系统有时会从授权服务器获取部分用户或客户端的详细信息并加以使用,而这些信息就是通过令牌来获取的。有时候令牌本身就包含所需信息(正如你将在第13.2节中阅读到的,这种令牌称为非不透明令牌);否则,后端就需要调用授权服务器来查询客户端和用户的信息(即不透明令牌)。另外,与实体钥匙不同,访问令牌并不会持续很久。它会在较短时间内(在大多数情况下为几分钟)过期,之后客户端需要再次向授权服务器申请新的令牌。这样一来,即便令牌遗失(就像丢失钥匙一样),也无法被滥用。
OAuth 2 描述了客户端可能获取令牌的多种流程,我们将这些流程称为“授权类型”,在 13.3 节中讨论了最常用的授权类型。
使用各种令牌实现¶
令牌是客户端在向后端(资源服务器)发送请求时用来获取授权的访问卡(见图13.4)。令牌是 OAuth 2 认证与授权流程中不可或缺的一环,它们用于证明客户端与用户身份的真实性,同时也是后端获取关于客户端和用户更多信息的途径。
在本节中,我们将介绍令牌的分类方式,以及根据不同的令牌类型,它们在授权流程中的具体应用。
我们根据令牌向资源服务器提供授权数据的方式对其进行分类:
- 不透明(Opaque)——此类令牌不存储数据。资源服务器通常需要调用授权服务器,提供不透明令牌以获取详细信息,从而实现授权。这个过程称为“审查调用”(introspection call)。
- 透明(Non-opaque)——此类令牌会存储数据,使后端能够立即进行授权判断。JSON Web Token(JWT)是最常用的透明令牌实现。
使用不透明令牌¶
不透明令牌本身不包含后端可以用来识别用户或客户端、也无法用于实施授权规则的数据。不透明令牌仅代表一次认证尝试的凭证。当资源服务器收到不透明令牌时,需要向授权服务器查询该令牌是否有效,并获取更多信息以便执行相应的授权约束。
不透明令牌就像是一个宝箱的钥匙。它本身不会提供任何信息;只有当你用它去打开宝箱时,才知道它是否有效。一旦确认有效,就能获取宝箱中的内容(在这个例子中是用户和客户端的详情)。图13.5对此进行了形象的说明。
资源服务器会调用授权服务器提供的一个端点,以确认不透明令牌是否有效,并获取关于令牌所发放的客户端和用户的必要信息。这个调用称为令牌内省(图13.6)。一旦资源服务器得到这些信息,它就可以施加相应的授权约束。
使用非不透明令牌¶
与13.2.1节讨论的不透明令牌不同,非不透明令牌包含了授权服务器在认证过程中向客户端和用户发放令牌时的信息。可以把非不透明令牌比作签名文档(图13.7)。
最常见的非不透明令牌实现是 JWT。JWT 由三部分组成(图 13.8):
- 头部——通常包含有关令牌的数据,比如用于签发令牌的加密算法或授权服务器用于签名的密钥 ID
- 体部——通常包含有关令牌签发对象的数据信息,比如客户端和用户详情
- 签名——通过加密生成的值,可用于证明授权服务器确实签发了该令牌,并且在生成后其头部或体部的内容未被篡改
头部和主体中的数据采用 JavaScript 对象表示法(JSON)格式,然后再进行 Base64 编码,以便体积更小、传输更方便。三个部分之间用点分隔。
下面的示例展示了一个 JWT,其三个部分均经过 Base64 编码,并以点号分隔:
你现在可能会问:“我应该什么时候使用不透明令牌,什么时候使用透明令牌?”正如我在本节前面提到的,目前最常用的是透明令牌,因为它们无需通过探测即可验证。然而,透明令牌中包含数据,客户端会将这些数据通过网络传给后端。任何获得该令牌的人也都能看到令牌所携带的数据。在大多数情况下这并不会造成问题,不过我建议尽量不要在令牌中携带过多数据。
但如果你需要携带的数据量较大,或者包含不适合通过令牌在网络上发送的敏感内容,该怎么办呢?在这种情况下,不透明令牌可能是一个不错的选择。我建议你首先考虑使用非不透明令牌,只有当令牌需要承载的数据量过大,或你必须传输一些更加敏感的细节、希望避免通过令牌来交换这些信息时,再退而选择不透明令牌。
通过多种授权类型获取令牌¶
本节介绍授权类型。授权类型是客户端获取令牌的流程。在应用中,你会看到各种客户端从授权服务器获取令牌的方式。我们将讨论三种最常用的授权类型。在本节末,我们还会探讨客户端在令牌过期后如何重新生成令牌。
Note
你可能还会发现有些应用在使用另外两种授权方式:隐式授权和密码授权。这两种授权方式已经不推荐使用,因为它们被认为安全性不足。我们在本书中不会深入讨论它们,也不建议在应用中使用它们。你完全可以用本节中讨论的其他授权方式来替代其中任意一种。如果你想进一步了解密码授权,可以参考本书第一版第十二章中的相关讨论。我们在本节讲解授权码授权时,也会简要回顾一下隐式授权以及它为什么被弃用。
第13.3.1节介绍了授权码授权类型,这是系统需要允许用户认证时最常用的一种授权方式。第13.3.2节讨论了对授权码授权类型的补充——码交换校验密钥(PKCE)。第13.3.3节继续介绍在应用需要在无需用户认证的情况下获取令牌的情形,最后第13.3.4节讲解了如何重新生成令牌。
使用授权码授权类型获取令牌¶
授权码授权类型是目前最常用的授权类型。当我们的应用需要对用户进行身份验证时就会使用它(要更容易理解这个授权类型,可以参考图 13.9,该图以顺序图形式展示了各步骤):
- 用户希望在所使用的应用中执行某项操作。例如,图左侧的女孩是玛丽,一位会计,她想查看公司需要支付的所有发票。
- 玛丽使用的应用是客户端。在这个例子中,玛丽坐在电脑前,所以她使用的客户端是一个 Web 应用。当然,玛丽也可以使用该应用的移动端。在两种情况下,该授权类型的流程是一样的。因为玛丽还未登录,应用会将她重定向到由授权服务器托管的登录页面。
- 现在玛丽在浏览器中看到登录页面。这个登录页面并不属于她访问的应用,而是由另一个系统托管的。玛丽识别出这个页面是她在公司工作中使用的统一认证应用。她知道提交凭据后,浏览器会将她带回发票应用,到时她就能查看发票并处理所需数据。 她填写了正确的凭据并点击登录按钮。
- 由于玛丽提供的凭据正确,授权服务器将她重定向回发票应用。同时,授权服务器还向初始应用(即客户端)发放一个名为“授权码”的唯一代码。客户端会使用该代码去换取访问令牌。
- 客户端请求访问令牌。客户端需要该令牌来向其后端(资源服务器)发送请求。
- 由于授权码正确(即步骤 4 中服务器提供的同一个代码),授权服务器返回访问令牌。
- 客户端应用使用访问令牌向后端发送请求并完成授权。
以下几点有助于你更好地理解这个流程:
- 注意那些虚线箭头。务必记住,它们表示的是浏览器中的重定向,而不是请求或响应。在第 2 步中,客户端应用会将用户重定向到授权服务器的登录页面(也就是在浏览器中重定向到另一个应用的网页)。在第 4 步中,授权服务器会重定向回客户端应用,并带上授权码(通常作为查询参数)。
- Mary(用户)并不清楚第 4 步到第 7 步的具体细节。她登录后,最终会看到客户端展示的发票,而这些发票是客户端在第 7 步获得授权后从服务器响应中获取的。
- 切记不要混淆授权码和访问令牌。客户端最终需要的是访问令牌,用来向自己的后端获得授权(第 7 步)。而要拿到访问令牌,客户端首先要获取授权码(第 4 步和第 5 步)。
此外,很多刚接触授权与认证的开发者在第 4 步时会感到困惑。我经常被问到:“为什么授权服务器在这里不直接返回访问令牌?”看起来客户本可以在第 4 步直接拿到令牌,却还需要再多走一步,确实让人觉得奇怪。
但这样做是合理的。事实上,在 OAuth 的最初版本中,授权服务器在第 4 步并不是提供授权码,而是直接返回访问令牌。这就是我们现在所说的“隐式授权类型”,该方式已被弃用且不再推荐使用。原因在于重定向请求很容易被拦截,而别有用心的人就可能轻而易举地获取访问令牌。通过返回授权码,授权服务器迫使客户端再次发送请求,并在其中重新使用凭证进行认证。这样一来,即使有人截获了重定向并拿到了授权码,光靠它也无法获取访问令牌。他们还必须掌握客户端凭证,才能发出请求并获取令牌。
图 13.10 直观地展示了授权码添加额外保护以防止他人获取访问令牌的两个步骤。
对授权码授权类型应用PKCE保护¶
如果有心怀不轨的人设法拿到了客户端凭证怎么办?在这种情况下,他们就可能获取访问令牌并向资源服务器发送请求。有什么办法能提前防范这种情况吗?当然有,授权码流程中新增的“交换代码用证明密钥”(PKCE,通常读作“pixy”)就是为了进一步强化安全性。接下来我们将探讨 PKCE 如何应对某人通过窃取客户端凭证进而获取访问令牌的情况。
仅在授权码授权类型的两个步骤中使用 PKCE,此前我们在第 13.3.1 节中已有讨论。在图 13.11 中,我将表示步骤 3 和步骤 5 的箭头加粗,这是应用 PKCE 的两个授权码授权类型的步骤:
- 首先,客户端需要生成一个随机值,可以是一段随机的字节字符串,这个值称为验证器。
- 接着,客户端对第一步中生成的随机值应用哈希函数。哈希函数是一种不可逆的加密方式,也就是说其输出无法还原为输入(见「密码管理」)。对验证器应用哈希函数后的结果称为挑战值。
客户端在第 3 步随用户登录一并发送挑战码。授权服务器保存该挑战码,并在第 5 步请求访问令牌时期待客户端提交校验码。如果客户端在第 5 步请求令牌时提交的校验码与第 3 步发送的挑战码一致,授权服务器就能确认发起令牌请求的客户端应用与最初请求用户认证的应用是同一个。
即便有人设法在第 4 步获得了 authorization code,也无法立即获取 access token。因为他们还需要知道 verifier 的值。而 verifier 还没被客户端发送出去,自然也就无法获知。即便在第 3 步截获了 challenge 也无济于事,因为 challenge 是通过哈希函数生成的,意味着输出无法反推回输入。
使用客户端凭证授权类型获取令牌¶
有时应用需要在没有用户干预的情况下获取授权。当场景中没有用户时,应用必须使用客户端凭证授权类型来获取访问令牌。这种情况通常发生在某个服务需要在客观事件(例如计划进程的定时器)触发时调用另一个服务。使用客户端凭证授权类型,应用只需使用其客户端凭证进行身份验证。图 13.12 展示了客户端凭证授权类型:
- 应用向授权服务器请求访问令牌,并使用其凭证进行身份验证。
- 如果凭证有效,授权服务器会颁发访问令牌。
- 应用在向资源服务器发送请求时,携带访问令牌以获得授权。
使用刷新令牌获取新的访问令牌¶
关于令牌,有一点必须牢记:它们的生命周期必须相对较短。具体能持续多久通常取决于应用场景,但一般不会超过15分钟,我也从未见过寿命超过一小时的令牌。归根结底,所有令牌终有失效的一天。一旦令牌过期,资源服务器就不再接受它。在这种情况下,如果客户端持有的令牌已失效,他有两个选择:
- 重新执行授权流程,获得新的访问令牌。这意味着在使用授权码流程时需要再次让用户登录。
- 使用刷新令牌来换取新的访问令牌。
刷新令牌在客户端使用需要用户登录的授权类型(如授权码)时尤其有用。想象一下,令牌只有15分钟的有效期。作为用户,如果你的应用每隔15分钟就让你重新登录一次,你会不会觉得很烦?我肯定会!
应用可以使用刷新令牌获取新的访问令牌,而不必在访问令牌过期时每次都让用户重新登录。
图13.13展示了使用刷新令牌的步骤:
- 用户尝试获取某些数据,这意味着客户端必须调用其后端。
- 由于之前获取的访问令牌已过期,客户端需要获取一个新的。客户端发送刷新令牌以证明此前已经通过认证的是同一方。
- 授权服务器识别刷新令牌,并为客户端提供新的访问令牌。
- 客户端可以使用新的访问令牌调用后端(资源服务器)并获得授权。
OpenID Connect为OAuth 2带来了什么¶
关于 OpenID Connect(有时简称 OIDC)和 OAuth 2 之间的关系,外面确实还存在不少迷惑。我通常会告诉我的学生别想太多:“只要你理解了 OAuth 2,也就掌握了怎么使用 OpenID Connect。”
事实上,OIDC 是建立在 OAuth 2 规范之上的协议。因此,理解 OAuth 2 能让你更容易掌握 OIDC。让我用一个类比来说明规范与协议之间的关系。
我们每天都在使用电源插座。世界各地的插座形状各异,出行时往往会带来不少麻烦。尤其是在跨不同地区旅行时,可能需要准备各种转换插头,才能保证设备随时充电。
但在幕后,所有插座的工作原理都是一样的。可以用简要的几条要点来概括全球所有电源插座的框架:
- 一个插座通常有三根导线用以传导电流:火线、零线和接地线,其中接地线是可选的。
- 插座提供的电压通常约为 120 伏或 230 伏。
现在不用担心自己不是技术人员;你不需要理解这两点。至少在学习 Spring Security 时不用。相信我就好。
问题在于,即便全世界的插座都符合这些规范,我们出行时仍然会遇到需要使用转换插头的情况。原因是这些插座并没有统一的协议,必须通过转换器将某一协议的插座适配到另一种(例如从北美标准转换到欧洲标准)。
在应用程序以及认证和授权方面也会出现同样的情况。即便两个应用都符合 OAuth 2 规范,它们仍可能因为实际运行的协议不完全一致而需要进行适配。OpenID Connect 是一个对 OAuth 2 规范做了适度限制的协议,引入了若干变更。主要变更包括:
- 对作用域(如 profile 或 openid)设定了具体的取值;
- 增加了一个名为 ID token 的额外令牌,用于存储关于用户以及获取该令牌的客户端的身份信息;
- 通常在 OIDC 语境下,授权类型(grant type)也会被称为流程(flow),而授权服务器则常被称为身份提供商(Identity Provider,IdP)。
OAuth 2 的弊端¶
本节讨论了使用 OAuth 2 进行认证与授权的应用可能存在的漏洞。了解在使用 OAuth 2 时可能出错的地方对于开发应用时规避这些情况至关重要。当然,和软件开发中的其他技术一样,OAuth 2 并非金刚不坏,它也有一些漏洞,我们在构建应用时必须心中有数。我在这里列出了一些最常见的:
- 客户端存在跨站请求伪造(CSRF)——如果用户已经登录,而应用没有任何 CSRF 保护机制,就可能遭遇 CSRF 攻击。我们在「配置CSRF防护」对 Spring Security 实现的 CSRF 保护机制有过深入讨论。
- 客户端凭证泄露——如果凭证在存储或传输过程中没有适当的保护,就可能创建让攻击者窃取并滥用的漏洞。
- 令牌重放——正如 13.2 节所述,令牌是在 OAuth 2 认证与授权架构中用于访问资源的“钥匙”。它们在网络中传输,有时可能被截获。被截获之后就等于被盗,可以被重复使用。想象一下你丢了家门的钥匙,会发生什么?别人就可以随意反复开门(重放)。 -
令牌劫持——攻击者扰乱认证流程并窃取令牌,用以访问资源。使用刷新令牌也存在类似的潜在风险,因为刷新令牌同样可能被拦截,并用来获取新的访问令牌。推荐阅读这篇实用文章:http://mng.bz/am5z
请记住,OAuth 2 本身是一个框架。漏洞往往源于在其基础上错误地实现功能。借助 Spring Security,我们的应用可以规避大多数这类漏洞。在使用 Spring Security 构建应用时(正如本章所展示的那样),我们需要配置相关设置,但仍然依赖 Spring Security 所实现的流程。
有关 OAuth 2 框架相关漏洞及其如何被不良分子利用的更多详情,可参见 Justin Richer 与 Antonio Sanso 合著的《OAuth 2 In Action》第 3 部分(Manning,2017 年),链接:http://mng.bz/g7Ql。
总结¶
- OAuth 2 框架描绘了后端认证其客户端的安全方式。OpenID Connect 是在 OAuth 2 客户端基础上施加一定约束、实现该协议的标准。
- OAuth 2 系统中的四个主要角色
- 用户——希望执行业务场景的个人
- 客户端——需要获得授权才能访问某个后端的资源或用例的应用
- 资源服务器——需要授权某个客户端访问特定资源或执行业务的后端
- 授权服务器——管理用户与客户端信息、支持其认证,并颁发可用于授权的令牌的应用
- 令牌是一张访问卡(或钥匙),客户端从授权服务器获取后,用于在受保护的后端(资源服务器)获取调用用例或访问特定资源的授权。
- 我们将令牌分为两类:
- 不透明令牌——令牌本身不包含发放对象(用户与客户端)的详细信息。对于此类令牌,资源服务器必须调用授权服务器进行令牌校验并获取授权所需的信息。这个令牌校验请求称为 introspection。
- 透明令牌——令牌中包含了发放对象(用户与客户端)的相关信息。透明令牌最常见的实现就是 JSON Web Token(JWT)。
- 客户端可以通过多种流程向授权服务器申请令牌。令牌颁发的这些流程称为授权类型。最常见的授权类型有
- 授权码授权类型
- 客户端凭证授权类型
- 有时我们会在授权码授权类型上附加额外安全措施,采用 PKCE(Proof Key for Code Exchange)方案。客户端通过额外参数避免恶意者通过窃取客户端凭证与授权码获取访问令牌的风险。
- 在特定场景下,应用可能需要在不重新认证用户的前提下续获取新的访问令牌。此类场景可使用刷新令牌。刷新令牌是一种只能用于获取新访问令牌的特殊令牌。