Spring Security in Action 第十二章 OAuth 2是如何工作的?

本专栏将从基础开始,循序渐进,以实战为线索,逐步深入SpringSecurity相关知识相关知识,打造完整的SpringSecurity学习步骤,提升工程化编码能力和思维能力,写出高质量代码。希望大家都能够从中有所收获,也请大家多多支持。
专栏地址:SpringSecurity专栏
本文涉及的代码都已放在gitee上:gitee地址
如果文章知识点有错误的地方,请指正!大家一起学习,一起进步。
专栏汇总:专栏汇总


本章包括

  • OAuth 2的概述
  • OAuth 2规范的介绍
  • 建立一个使用单点登录的OAuth 2应用程序

如果你已经在使用OAuth 2,我知道你在想什么:OAuth 2框架是一个庞大的主题,在四章中,你会学到所有你需要知道的关于用Spring Security应用OAuth 2的知识。在这一章中,我们将从概述开始,你会发现OAuth 2框架的主要角色是用户、客户端、资源服务器和授权服务器。在总体介绍之后,你将学习如何使用Spring Security来实现客户端。然后,在第13章到第15章,我们将讨论实现最后两个组件:资源服务器和授权服务器。

为了达到这个目标,在本章中,我们将讨论什么是OAuth 2,然后我们将把它应用于一个专注于单点登录(SSO)的认证的应用。我之所以喜欢用SSO的例子来讲授这个课题,是因为它非常简单,但也非常有用。它提供了一个关于OAuth 2的概述,并让你在不写太多代码的情况下实现一个完全有效的应用程序,从而获得满足感。

在第13章到第15章中,我们将把本章所涉及的内容应用于你在本书前几章中已经熟悉的代码实例中。一旦我们完成这四章,你就会对在你的应用程序中用Spring Security实现OAuth 2所需要的东西有一个很好的概念。

由于OAuth 2是一个很大的主题,我将在适当的地方提到我认为必要的知识。 Spring Security让用OAuth 2开发应用程序变得很容易。你需要开始的唯一先决条件是本书的第2章到第11章,在这些章节中你学到了Spring Security中认证和授权的一般架构。我们将讨论的关于OAuth 2的内容基于Spring Security的标准授权和认证架构。

12.1 OAuth 2框架

在本节中,我们将讨论OAuth 2框架。今天,OAuth 2被普遍用于保障网络应用的安全,所以你可能已经听说过它了。你很有可能需要在你的应用程序中应用OAuth 2。这就是为什么我们需要讨论在Spring应用程序中使用Spring Security来应用OAuth 2。我们从一点理论开始,然后在一个使用SSO的应用程序中应用它。

在大多数情况下,OAuth 2被称为一个授权框架(或规范框架),其主要目的是允许第三方网站或应用程序访问资源。有时,人们把OAuth 2称为一个委托协议。无论你选择如何称呼它,重要的是要记住,OAuth 2不是一个具体的实现或一个库。你也可以用其他平台、工具或语言来应用OAuth 2的流程定义。在本书中,你会发现如何用Spring Boot和Spring Security实现OAuth 2。

我认为理解OAuth 2是什么以及它的用处的一个好方法是用我们在本书中已经分析过的例子开始讨论。到目前为止,你在大量的例子中看到的最微不足道的认证方式是HTTP Basic认证方式。这对我们的系统来说还不够,对于HTTP Basic认证,我们有两个问题需要考虑到:

  • 为每一个请求发送凭证(图12.1)
  • 由一个单独的系统管理用户的凭证

image-20230129140416400

图12.1 当使用HTTP基本认证时,你需要在所有请求中发送凭证并重复认证逻辑。这种方法意味着经常在网络上分享凭证。

为每一个请求发送证书可能在个别情况下有效,但这通常是不可取的,因为它意味着

  • 经常通过网络共享凭证
  • 让客户(浏览器,在网络应用的情况下)以某种方式存储证书,以便客户可以将这些证书与请求一起发送给服务器,以获得认证和授权。

我们想从我们的应用程序的架构中摆脱这两点,因为它们削弱了安全性。大多数情况下,我们希望有一个单独的系统来管理用户凭证。想象一下,你必须为你组织中的所有应用程序配置和使用单独的凭证(图12.2)。

image-20230129140609634

图12.2 在一个组织中,你与多个应用程序一起工作。其中大部分都需要你进行认证才能使用。对你来说,知道多个密码是一种挑战,对组织来说,管理多套凭证也是一种挑战。

如果我们把凭证管理的责任隔离在系统的一个组件中,那就更好了。我们暂且称它为授权服务器(图12.3)。

image-20230129140822548

图12.3 一个更容易维护的架构将凭证分开保存,并允许所有应用程序为其用户使用同一套凭证。

这种方法消除了代表同一个人的证书的重复性。通过这种方式,架构变得更简单,更容易维护。

12.2 OAuth 2认证架构的组成部分

在本节中,我们将讨论在OAuth 2在认证中发挥作用的组件。你需要了解这些组件和它们所扮演的角色,因为我们在接下来的章节中会提到它们。但在本节中,我们只讨论这些组件是什么以及它们的用途(图12.4)。正如你将在第12.3节中所了解的,这些组件之间有更多的交互方式。而且,在那一节中,你还会了解到导致这些组件之间不同交互的不同流程。

如前所述,OAuth 2组件包括

  • 资源服务器-托管用户拥有的资源的应用程序。 资源可以是用户的数据或他们的授权行为。
  • 用户(也被称为资源所有者)–拥有资源服务器所提供的资源的个人。一个用户通常有一个用户名和密码,用来识别自己。
  • 客户端–代表用户访问其拥有的资源的应用程序。
  • 授权服务器–授权客户端访问资源服务器所提供的用户资源的应用程序。当授权服务器决定客户端被授权代表用户访问某项资源时,它就会发出一个token。客户端使用这个token向资源服务器证明它是由授权服务器授权的。如果客户端有一个有效的token,资源服务器就允许它访问它所请求的资源。

image-20230129141603313

图12.4 OAuth 2架构的主要组成部分是资源所有者、客户端、授权服务器和资源服务器。每一个都有自己的职责,这在认证和授权过程中是至关重要的。

12.3 OAuth 2的使用

在本节中,我们将讨论如何应用OAuth 2,这取决于你的应用程序的架构。正如你将了解到的,OAuth 2意味着多种可能的认证流程,你需要知道哪一种适用于你的情况。在本节中,我将采取最常见的情况,并对这些情况进行评估。重要的是,在开始第一个实现之前,要先做这件事,以便你知道你在实现什么。

那么,OAuth 2是如何工作的?实现OAuth 2的认证和授权是什么意思?简而言之,OAuth 2是指使用token进行授权。 请记住11.2节,token就像访问卡。一旦你获得一个token,你就可以访问特定的资源。

但OAuth 2提供了多种获得token的可能性,称为授予。下面是最常见的OAuth 2授权,你可以选择。

  • 授权码
  • 密码
  • 刷新token
  • 客户端凭证

12.3.1 授权码授予类型的使用

在本节中,我们将讨论授权码授予类型(图12.5)。我们还将在第12.5节中实现的应用程序中使用它。该授予类型是最常用的OAuth 2流程之一,所以了解它的工作原理和如何应用它是相当重要的。你很有可能会在你开发的应用程序中使用它。

image-20230129143415258

图12.5 授权码授予类型。客户端要求用户直接与授权服务器交互,为用户的请求授予许可。一旦获得授权,授权服务器就会发出一个token,客户端使用该token来访问用户的资源。

图12.5中的箭头不一定代表HTTP请求和响应。例如,当客户端告诉用户(图中顶部的第二个箭头),“告诉授权服务器你允许我做这个动作”,客户端就会把用户重定向到授权服务器的登录页面。当授权服务器给客户端一个token时,授权服务器实际上是通过我们所说的重定向URI来调用客户端。 你将在第12章到第15章中学习所有这些细节,所以现在不用担心。通过这个说明,我想让你知道,这些顺序图并不只是代表HTTP请求和响应。 这些是对OAuth 2行为体之间通信的简化描述。

以下是授权码授予类型的工作方式。在这之后,我们深入了解每个步骤的细节。

  1. 提出认证请求
  2. 获得一个访问token
  3. 调用受保护的资源

第一步:用授权码授予类型提出认证请求

客户端将用户重定向到授权服务器的一个api,在那里他们需要进行认证。你可以想象你正在使用应用程序X,你需要访问一个受保护的资源。为了访问该资源,App X需要你进行身份验证。 它为你打开一个页面,在授权服务器上有一个登录页,你必须用你的证书来填写。

从技术上讲,这里发生的情况是,当客户端将用户重定向到授权服务器时,客户端调用授权端点,请求查询中包含以下细节:

  • response_type的值为code,它告诉授权服务器,客户端希望有一个code。客户端需要这个code来获得一个访问token,你将在第二步中看到。
  • client_id,其值为客户端ID,用于识别应用程序本身。
  • redirect_uri,它告诉授权服务器在成功认证后将用户重定向到哪里。有时授权服务器已经知道每个客户的默认重定向URI。由于这个原因,客户端不需要发送重定向URI。
  • scope,这与我们在第五章中讨论的授予权限相似。
  • state,其中定义了一个跨站请求伪造(CSRF)token,用于我们在第10章讨论的CSRF保护。

认证成功后,授权服务器重定向URI,并提供一个code和state。客户端检查state是否与它在请求中发送的state相同,以确认它不是其他人在试图调用重定向URI。客户端使用该code来获得步骤2中提出的访问token。

步骤2:获得具有授权码授予类型的访问token

为了允许用户访问资源,步骤1产生的code是客户端证明用户认证的证据。你猜得没错,这就是为什么这被称为授权代码授予类型。现在,客户端用code调用授权服务器以获得token。

image-20230129145424009

图12.6 第一步意味着用户和授权服务器之间的直接互动。在这第二步中,客户向授权服务器请求一个访问token,并提供在第一步中获得的授权码。

在许多情况下,这前两步会造成混乱。人们通常不明白为什么这个流程需要两次调用授权服务器和两个不同的token–授权码和访问token。花点时间来理解这一点。

  • 授权服务器生成第一个code,作为用户直接与之互动的证明。客户端收到这个code后,必须用它和它的凭证再次进行认证,以获得一个访问token。
  • 客户端使用第二个token来访问资源服务器上的资源。

那么,为什么授权服务器没有直接返回第二个token(访问token)?嗯,OAuth 2定义了一个叫做隐式授予类型的流程,授权服务器直接返回一个访问token。本节没有列举隐式授予类型,因为不建议使用这种类型,而且现在大多数授权服务器都不允许这样做。授权服务器直接用访问token调用重定向URI,而不确定它确实是接收该token的正确客户端,这个简单的事实使得流程的安全性降低。通过先发送一个授权码,客户必须通过使用他们的证书来获得一个访问token来再次证明他们是谁。客户端进行最后一次调用以获得访问token,并发送

  • 授权码,证明用户对其进行了授权
  • 他们的证书,这证明他们确实是同一个客户,而不是其他截获授权码的人

回到第2步,从技术上讲,客户端现在向授权服务器提出了一个请求。这个请求包含以下细节:

  • code,这是在步骤1中收到的授权代码。这就证明了用户的身份验证。
  • client_id 和 client_secret 客户的凭证。
  • redirect_uri 这与步骤1中的一样。
  • grant_type grant_type对应的值为authority_code,它标识了所使用的流的种类。一个服务器可能支持多个认证流,所以必须指定哪个是当前执行的认证流。

作为响应,服务器会发回一个access_token。这个token是一个值,客户可以用它来调用资源服务器所暴露的资源。

第3步:用授权码授予类型调用受保护资源

在成功地从授权服务器获得访问token后,客户端现在可以调用受保护的资源了。客户端在调用资源服务器的api时,在授权请求头中使用访问token。

在本节的最后,我用一个比喻来说明这种流动。我有时会从一家我认识很久的小店买书。我必须提前订书,然后在几天后取书。但这家店不在我的日常上下班路线上,所以有时我不能自己去取书。我通常会请住在我附近的朋友去那里帮我取书。当我的朋友询问我的订单时,商店的女士会打电话给我,确认我已经派人去取书了。在我确认后,我的朋友就会去取包裹,并在当天晚些时候把它带给我。

在这个比喻中,书籍是资源。我拥有它们,所以我是用户(资源所有者)。为我取书的朋友是client。卖书的女士是授权服务器。(我们也可以把她或书店看作是资源服务器。)请注意,为了授予我的朋友(客户)取书(资源)的许可,卖书的女士(授权服务器)直接给我(用户)打电话。这个类比描述了授权代码和隐式授予类型的过程。当然,由于我们在故事中没有token,这个类比是局部的,描述了两种情况。

授权码授予类型有一个很大的优点,就是使用户能够允许客户端执行特定的动作,而不需要与客户端分享他们的凭证。但这种授予类型有一个弱点:如果有人拦截了授权代码,会发生什么?当然,客户端需要用它的凭证进行验证,正如我们之前讨论的那样。但是,如果客户端的凭证也被盗了呢?即使这种情况不容易实现,我们也可以认为它是这种授予类型的漏洞。

12.3.2 密码授予类型的使用

在本节中,我们讨论密码授予类型(图12.7)。这种授予类型也被称为资源所有者凭证授予类型。使用这种流程的应用程序假定客户端收集用户凭证,并使用这些凭证来验证和从授权服务器获得访问token。

还记得我们在第11章中的实战例子吗?我们实现的架构与密码授予类型中的情况非常接近。在第13章到第15章,我们还用Spring Security实现了一个真正的OAuth 2密码授予类型架构。

这时你可能会问,资源服务器是如何知道一个token是否有效的。在第13章和第14章,我们将讨论资源服务器用来验证token的方法。目前,你应该把注意力集中在授予类型的讨论上,因为我们只提到了授权服务器如何发放访问token。

image-20230129151148384

图12.7 密码授予类型假定用户与客户端共享他们的凭证。 客户端使用这些凭证从授权服务器获得一个token。然后,它代表用户从资源服务器上访问资源。

只有当客户端和授权服务器是由同一个组织建立和维护时,你才会使用这个认证流程。为什么呢?让我们假设你建立了一个微服务系统,你决定将认证责任分离为一个不同的微服务,以提高可扩展性并保持每个服务的责任分离。 这种分离在许多系统中被广泛使用。

让我们进一步假设,你的系统的用户使用的是一个用Angular、ReactJS或Vue.js等前端框架开发的客户端Web应用,或者他们使用的是一个移动应用程序。在这种情况下,用户可能会认为从你的系统被重定向到同一个系统进行认证,然后再返回是很奇怪的事情。这就是像授权码授予类型的流程会发生的情况。对于密码授予类型,你会期望应用程序向用户提供一个登录表单,并让客户端负责向服务器发送凭证来进行认证。用户不需要知道你是如何在你的应用程序中设计认证责任的。让我们看看在使用密码授予类型时会发生什么。 这两个任务如下。

  • 请求一个访问token。
  • 使用访问token来调用资源。

第1步:使用密码授予类型时请求访问token

密码授予类型的流程要简单得多。客户端收集用户凭证并调用授权服务器以获得一个访问token。当请求获得访问token时,客户端还在请求中发送以下细节。

  • grant_type,值为password
  • client_id 和 client_secret,是客户用来认证自己的凭证。
  • scope,它代表了被授予的权力
  • username 和password 是用户凭证。这些是以纯文本形式作为请求头的值发送的。

客户端在响应中收到一个访问token。客户端现在可以使用该访问token来调用资源服务器的端点。

第2步:使用密码授予类型时,使用访问token来调用资源

一旦客户端有了访问token,它就会使用该token来调用资源服务器上的api,这与授权码授予类型完全一样。客户端将访问token添加到授权请求头的请求中。

回到我在第12.3.1节所做的类比,想象一下,卖书的女士没有给我打电话确认我想让我的朋友去拿书。我反而会把我的身份证给我的朋友,以证明我委托我的朋友去取书。看到区别了吗?在这个流程中,我需要与客户分享我的ID(凭证)。出于这个原因,我们说这种授予类型只适用于资源所有者 "信任 "客户端的情况。

密码授予类型不如授权码授予类型安全,主要是因为它假定与客户端应用程序共享用户凭证。虽然它确实比授权码授予类型更直接,但在现实世界的场景中尽量避免这种授予类型。即使授权服务器和客户端都是由同一个组织建立的,你也应该首先考虑使用授权码授予类型。 把密码授予类型作为你的第二个选择。

12.3.3 客户凭证授予类型的使用

在本节中,我们将讨论客户凭证授予类型(图12.8)。这是OAuth 2所描述的授予类型中最简单的一种。我喜欢把客户凭证授予类型看作是密码授予类型和API密钥认证流程的结合。我们假设你有一个用OAuth 2实现认证的系统,现在你需要允许外部服务器进行认证并调用你的服务器所暴露的特定资源。

image-20230129153751599

图 12.8 客户端凭证授予类型。如果客户需要访问一个资源,但不代表资源所有者,我们就使用这个流程。这个资源可以是一个不属于用户的api。

客户凭证授予类型的步骤与密码授予类型类似。唯一的例外是,访问token的请求不需要任何用户凭证。下面是实现这种授予类型的步骤:

  1. 请求一个访问token
  2. 使用访问token来调用资源

第1步:用客户凭证授予类型请求访问token

为了获得一个访问token,客户端向授权服务器发送一个请求,并提供以下细节。

  • grant_type,值为client_credentials。
  • client_id和client_secret,代表客户的证书。
  • scope,它代表了被授予的权力

作为响应,客户端会收到一个访问token。客户端现在可以使用该访问token来调用资源服务器的端点。

第2步:使用访问token来调用具有客户凭证授予类型的资源

一旦客户端有了访问token,它就会使用该token来调用资源服务器上的api,这与授权码授予类型和通字授予类型完全一样。客户端将访问token添加到授权请求头中。

12.3.4 使用刷新token来获得新的访问token

在本节中,我们讨论刷新token(图12.9)。到目前为止,你已经了解到,OAuth 2流程的结果,我们也称之为授予,是一个访问token。但我们并没有对这个token说太多。最后,OAuth 2并没有为token假设一个特定的实现。你现在要学习的是,无论如何实现,token都会过期。这不是强制性的–你可以创建具有无限寿命的token–但是,一般来说,你应该使这些token尽可能短命。我们在本节中讨论的刷新token代表了一种获得新访问token的替代方法,即使用信用证。我向你展示了刷新token在OAuth 2中的工作原理,你还会在第13章看到这些token在一个应用程序中的实现。

让我们假设在你的应用程序中,你实现了永不过期的token。这意味着客户端可以反复使用同一个token来调用资源服务器上的资源。

如果token被盗怎么办?最后,别忘了,token是作为一个简单的HTTP头附在每一个请求上的。如果token不会过期,拿到token的人就可以用它来访问资源。一个不会过期的token是太强大了。它几乎变得和用户凭证一样强大。我们倾向于避免这种情况,并使token的有效期短一些。这样,在某些时候,过期的token就不能再使用了。客户端必须获得另一个访问token。

为了获得一个新的访问token,客户端可以重新运行流程,这取决于所使用的授予类型。例如,如果授予类型是认证码,客户端会将用户重定向到授权服务器的登录api,用户必须再次填写他们的用户名和密码。这对用户并不友好,不是吗?想象一下,token有20分钟的寿命,而你用在线应用工作了几个小时。在这段时间里,应用程序会重定向你,让你再次登录。 了避免重新认证的需要,授权服务器可以发出一个刷新token,它的价值和目的与访问token不同。应用程序使用刷新token获得一个新的访问token,而不需要重新认证。

在密码授予类型中,刷新token也比重新认证有优势。即使是密码授予类型,如果我们不使用刷新token,我们将不得不要求用户再次认证或存储他们的凭证。在使用密码授予类型时,存储用户凭证是你可能犯的最大错误之一。而且我已经看到这种方法在实际应用中的使用!请不要这样做!如果你存储了用户名和密码(假设你把这些保存为明文或可逆的东西,因为你必须能够重复使用它们),你就暴露了这些凭证。刷新token可以帮助你轻松、安全地解决这个问题。你可以存储一个刷新token,并在需要时使用它来获得一个新的访问token,而不是不安全地存储凭证,也不需要每次都重定向用户。存储刷新token是比较安全的,因为如果你发现它被暴露,你可以撤销它。此外,不要忘记,人们往往对多个应用程序拥有相同的凭证。因此,丢失凭证比丢失一个可以在特定应用中使用的token更糟糕。

最后,让我们看看如何使用刷新token。你从哪里得到一个刷新token?当使用像授权码或密码授予类型的流程时,授权服务器会将刷新token与访问token一起返回。对于客户凭证授予,没有刷新token,因为这个流程不需要用户凭证。一旦客户端有了刷新token,当访问token过期时,客户端应该发出一个包含以下细节的请求:

  • grant_type的值为refresh_token。
  • refresh_token,其值为刷新token的值。
  • client_id和client_secret。
  • scope 其中定义了相同的授予权限或更少。如果需要授权更多的授予权限,则需要重新认证。

作为对该请求的响应,授权服务器发出一个新的访问token和一个新的刷新token。

12.4 OAuth 2的缺点

在本节中,我们将讨论OAuth 2认证和授权可能存在的漏洞。了解在使用OAuth 2时可能出现的问题是很重要的,这样你就可以避免这些情况。当然,就像软件开发中的其他东西一样,OAuth 2并不是无懈可击的。它有自己的弱点,我们必须意识到这些弱点,并且在构建我们的应用程序时必须考虑这些弱点。我在这里列举了一些最常见的。

  • 在客户端使用跨站请求伪造(CSRF)–在用户登录的情况下,如果应用程序没有应用任何CSRF保护机制,CSRF是可能发生的。我们在第10章中对Spring Security实现的CSRF保护进行了讨论。
  • 窃取客户凭证–存储或转移未受保护的凭证会产生漏洞,使攻击者能够窃取和使用这些凭证。
  • 重放token–正如你将在第13章和第14章中所学到的,token是我们在OAuth 2认证和授权架构中用来访问资源的钥匙。你通过网络发送这些东西,但有时,它们可能会被截获。如果被截获,它们就会被盗,并可以被重新使用。想象一下,你丢失了你家前门的钥匙。可能会发生什么?其他人可以用它来打开门,只要他们愿意。我们将在第14章学习更多关于token的知识,以及如何避免token重放。
  • token劫持-暗示有人干扰认证过程并窃取他们可以用来访问资源的token。这也是使用刷新token的一个潜在漏洞,因为这些token也可以被拦截并用于获得新的访问token。

记住,OAuth 2是一个框架。漏洞是在它上面错误地实现功能的结果。使用Spring Security已经可以帮助我们减轻应用程序中的大部分漏洞。

12.5 实现一个简单的单点登录应用程序

在本节中,我们将实现本书中第一个使用OAuth 2框架与Spring Boot和Spring Security的应用。这个例子向你展示了如何用Spring Security应用OAuth 2的总体概况。顾名思义,单点登录(SSO)应用是指你通过授权服务器进行认证,然后应用使用刷新token让你保持登录状态。在我们的案例中,它只代表OAuth 2架构中的client。

在这个应用中(图12.10),我们使用GitHub作为授权和资源服务器,并着重于组件之间的通信与授权代码授予类型。在第13章和第14章中,我们将在一个OAuth 2架构中同时实现授权服务器和资源服务器。

image-20230129160039276

图 12.10 我们的应用程序在 OAuth 2 架构中扮演着客户端的角色。我们使用GitHub作为授权服务器,但它也承担了资源服务器的角色,这使我们能够检索到用户的详细信息。

12.5.1 管理授权服务器

在这一节中,我们将配置授权服务器。在本章中,我们不会实现我们自己的授权服务器,而是使用一个现有的服务器。GitHub。在第13章,你将学习如何实现自己的授权服务器。

那么,我们应该如何使用像GitHub这样的第三方作为授权服务器呢? 这意味着,最终我们的应用程序不会管理其用户,任何人都可以使用他们的GitHub账户登录我们的应用程序。就像其他授权服务器一样,GitHub 需要知道它向哪个客户端应用程序发放token。 记得在第 12.3 节中,我们讨论了 OAuth 2 的授予,请求使用了一个客户端 ID 和一个客户端密码。客户端使用这些凭证在授权服务器上验证自己,因此OAuth应用程序必须在GitHub授权服务器上注册。要做到这一点,我们要使用下面的链接完成一个简短的表单提交(图12.11)。

https://github.com/settings/applications/new

image-20230129160427566

图 12.11 要把你的应用程序作为 OAuth 2 客户端,以 GitHub 作为授权服务器,你必须先注册它。你可以通过填写表格在 GitHub 上添加一个新的 OAuth 应用程序来完成。

当你添加一个新的OAuth应用时,你需要为该应用指定一个名称,主页,以及GitHub将对你的应用进行回调的链接。与此相关的OAuth 2授予类型是授权码授予类型。这种授予类型假定客户端将用户重定向到授权服务器(在我们的例子中是GitHub)进行登录,然后授权服务器在定义的URL上回调客户端,正如我们在12.3.1节所讨论的那样。这就是为什么你需要在这里确定回调URL的原因。因为我在自己的系统上运行这个例子,所以我在两种情况下都使用localhost。而且因为我没有改变端口(默认是8080,你已经知道了),这使得http://localhost:8080 我的主页URL。我在回调中使用相同的URL。

一旦你填完表格并选择注册申请,GitHub 就会为你提供一个客户端 ID 和一个客户端密码(图 12.12)。

image-20230129160811209

图 12.12 当你在 GitHub 注册一个 OAuth 应用时,你会收到客户端的凭证。你可以在你的应用程序配置中使用这些证书。

15a2c5a0e3fe921a223d
43cfa8dea92894ed7c26ec1ced76ee9354e15e15

注意 我删除了你在图片中看到的应用程序。因为这些证书提供了对机密信息的访问,我不能让它们继续存在。由于这个原因,你不能重复使用这些凭证;你需要按照本节的要求生成你自己的凭证。另外,在使用这些凭证编写应用程序时要小心,尤其是在使用公共的 Git 仓库来存储这些凭证时。

这个配置是我们为授权服务器所需要做的一切。现在我们有了客户证书,我们就可以开始开发我们的应用程序了。

12.5.2 开始实现

在本节中,我们开始实现一个SSO应用。我们创建一个新的Spring Boot应用程序,并在pom.xml文件中添加以下的依赖项。

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
</dependency>

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-security</artifactId>
</dependency>

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-oauth2-client</artifactId>
</dependency>

我们首先需要一个web页面。为了做到这一点,我们创建了一个controller类和一个简单的HTML页面来代表我们的应用程序。下面列出了MainController类,它定义了我们应用程序的单一api。

代码清单12.1 controller类

package com.hashnode.proj0001firstspringsecurity.controller;

import org.springframework.security.oauth2.client.authentication.OAuth2AuthenticationToken;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;

import java.util.logging.Logger;

@Controller
public class MainController {
    
    

    private Logger logger =
            Logger.getLogger(MainController.class.getName());

    @GetMapping("/")
    public String main(OAuth2AuthenticationToken token) {
    
    
        logger.info(String.valueOf(token));
        return "main.html";
    }
}

我还在我的Spring Boot项目的资源/静态文件夹中定义了main.html页面。它只包含标题文本,以便我在访问该页面时可以观察到以下情况:

Hello there!

现在是真正的工作!让我们来设置安全配置,让我们的应用程序能够使用GitHub的登录方式。我们先写一个配置类,就像我们习惯的那样。 我们扩展WebSecurityConfigurerAdapter并重写configure(HttpSecurity http)方法。现在有一个不同之处:我们没有像在第四章中学到的那样使用httpBasic()或formLogin(),而是调用一个不同的方法,名为oauth2Login()。这段代码在下面的代码中呈现。

代码清单12.2 配置类

package com.hashnode.proj0001firstspringsecurity.config;

import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;

@Configuration
public class ProjectConfig extends WebSecurityConfigurerAdapter {
    
    
    @Override
    protected void configure(HttpSecurity http) throws Exception {
    
    
        //设置认证方法
        http.oauth2Login();

        //指定一个用户需要经过认证才能提出请求
        http.authorizeRequests()
                .anyRequest().authenticated();
    }
}

在代码清单12.2中,我们在HttpSecurity对象上调用一个新方法:oauth2Login()。和httpBasic()或formLogin()一样,oauth2Login()只是在过滤器链中添加了一个新的认证过滤器。 我们在第9章中讨论了过滤器,在那里你了解到Spring Security有一些filter实现,你也可以在过滤器链中添加自定义的。在这种情况下,当你调用oauth2Login()方法时,框架添加到过滤器链的过滤器是OAuth2LoginAuthenticationFilter(图12.13)。这个过滤器拦截请求,并为OAuth 2认证应用所需的逻辑。

image-20230129162128639

图12.13 通过调用HttpSecurity对象的oauth2Login()方法,我们将OAuth2LoginAuthenticationFilter添加到过滤器链中。它拦截请求并应用OAuth 2认证逻辑。

12.5.3 实现客户注册

在本节中,我们将讨论如何实现OAuth 2客户端和授权服务器之间的链接。如果你想让你的应用程序真正有所作为,这一点至关重要。 如果你现在按原样启动它,你将无法访问主页面。不能访问该页面的原因是,你指定了对于任何请求,用户都需要进行认证,但你没有提供任何认证的方式。我们需要确定 GitHub 是我们的授权服务器。为此,Spring Security定义了ClientRegistration接口。

ClientRegistration接口代表OAuth 2架构中的客户端。对于客户端,你需要定义其所有需要的细节,其中我们有:

  • 客户端ID和密码
  • 用于认证的授予类型
  • 重定向URI
  • 范围

你可能还记得第12.3节,应用程序需要所有这些细节来进行认证过程。Spring Security还提供了一种简单的方法来创建一个构建器的实例,类似于你从第2章开始用来构建UserDetails实例的方法。代码清单12.3显示了如何用Spring Security提供的构建器来构建这样一个实例,来代表我们的客户端实现。

代码清单12.3 创建一个ClientRegistration实例

private ClientRegistration clientRegistration() {
    
    
    ClientRegistration cr = ClientRegistration.withRegistrationId("github")
            .clientId("a7553955a0c534ec5e6b")
            .clientSecret("1795b30b425ebb79e424afa51913f1c724da0dbb")
            .scope(new String[]{
    
    "read:user"})
            .authorizationUri("https://github.com/login/oauth/authorize")
            .tokenUri("https://github.com/login/oauth/access_token")
            .userInfoUri("https://api.github.com/user")
            .userNameAttributeName("id")
            .clientName("GitHub")
            .authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE)
            .redirectUriTemplate("{baseUrl}/{action}/oauth2/code/{registrationId}")
            .build();
    return cr;
}

所有这些细节是怎么来的?我知道清单12.3第一眼看上去很吓人,但它只不过是设置了客户的ID和密码。另外,在清单12.3中,我定义了作用域(授予的权限)、一个客户名称和一个我选择的注册ID。除了这些细节,我还必须提供授权服务器的URL:

  • Authorization URI 客户端重定向用户进行认证的URI。
  • Token URI 客户端为获得访问token和刷新token而调用的URI,如12.3节所述
  • User info URI 客户端在获得访问token后可以调用的URI,以获得关于用户的更多细节。

我是从哪里得到这些URI的?好吧,如果授权服务器不是你开发的,就像我们的情况一样,你需要从文档中获取它们。以GitHub为例,你可以在这里找到它们:

https://developer.github.com/apps/building-oauth-apps/authorizing-oauth-apps/

Spring Security甚至比这更聪明。该框架定义了一个名为CommonOAuth2Provider的类。这个类部分地定义了你可以用于认证的最常见的提供者的ClientRegistration实例,其中包括:

  • Google
  • GitHub
  • Facebook
  • Okta

清单12.4 使用CommonOAuth2Provider类

private ClientRegistration clientRegistration() {
    
    
    return CommonOAuth2Provider.GITHUB.getBuilder("github")
            .clientId("a7553955a0c534ec5e6b")
            .clientSecret("1795b30b425ebb79e424afa51913f1c724da0dbb")
            .build();
}

正如你所看到的,这要干净得多,你不必手动寻找和设置授权服务器的URL。当然,这只适用于普通提供商。如果你的授权服务器不在常见的提供者之列,那么你没有其他选择,只能完全按照清单12.3中的定义ClientRegistration。

注意 使用CommonOAuth2Provider类的值也意味着你依赖于你使用的提供者不会改变URL和其他相关值。虽然这不太可能,但如果你想避免这种情况,可以选择实现代码清单12.3中的ClientRegistration。这使得你可以在配置文件中配置URL和相关的提供者的值。

在本节的最后,我们为配置类添加了一个私有方法,该方法将返回ClientRegistration对象,如下表所示。在第12.5.4节中,你将学习如何为Spring Security注册这个客户端注册对象,以便将其用于验证。

清单12.5 在配置类中建立ClientRegistration对象

package com.laurentiuspilca.ssia.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.config.oauth2.client.CommonOAuth2Provider;
import org.springframework.security.oauth2.client.registration.ClientRegistration;
import org.springframework.security.oauth2.client.registration.ClientRegistrationRepository;
import org.springframework.security.oauth2.client.registration.InMemoryClientRegistrationRepository;
import org.springframework.security.oauth2.core.AuthorizationGrantType;

@Configuration
public class ProjectConfig extends WebSecurityConfigurerAdapter {
    
    


    private ClientRegistration clientRegistration() {
    
    
        return CommonOAuth2Provider.GITHUB.getBuilder("github")
                .clientId("a7553955a0c534ec5e6b")
                .clientSecret("1795b30b425ebb79e424afa51913f1c724da0dbb")
                .build();
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
    
    
        http.oauth2Login();

        http.authorizeRequests()
                .anyRequest().authenticated();
    }
}

12.5.4 实现ClientRegistrationRepository

在这一节中,你将学习如何为Spring Security注册ClientRegistration实例,以用于认证。。为此,Spring Security使用一个ClientRegistrationRepository类型的对象(图12.14)。

image-20230129164239675

图12.14 ClientRegistrationRepository 检索ClientRegistration细节(客户ID,客户密码,URL,作用域,等等)。认证过滤器需要这些细节用于认证流程。

ClientRegistrationRepository接口类似于UserDetailsService接口,你在第二章已经了解到。就像UserDetailsService对象通过用户名找到UserDetails一样,Client-RegistrationRepository对象通过注册ID找到ClientRegistration。

你可以实现ClientRegistrationRepository接口来告诉框架在哪里可以找到ClientRegistration实例。Spring Security为我们提供了一个ClientRegistrationRepository的实现,它将ClientRegistration的实例存储在内存中。正如你所猜测的,这与InMemoryUserDetailsManager对UserDetails实例的工作原理类似。我们在第三章讨论了InMemoryUserDetailsManager。

为了结束我们的应用实现,我使用InMemoryClientRegistrationRepository实现定义了一个ClientRegistrationRepository,并将其注册为Spring上下文中的一个bean。

我把我们在12.5.3节中建立的ClientRegistration实例添加到InMemoryClientRegistrationRepository中,把它作为参数提供给InMemoryClientRegistrationRepository构造函数。你可以在下一个代码清单中找到这段代码。

代码清单12.6 注册ClientRegistration对象

  • 方式1
package com.hashnode.proj0001firstspringsecurity.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.config.oauth2.client.CommonOAuth2Provider;
import org.springframework.security.oauth2.client.registration.ClientRegistration;
import org.springframework.security.oauth2.client.registration.ClientRegistrationRepository;
import org.springframework.security.oauth2.client.registration.InMemoryClientRegistrationRepository;

@Configuration
public class ProjectConfig extends WebSecurityConfigurerAdapter {
    
    

    private ClientRegistration clientRegistration() {
    
    
        return CommonOAuth2Provider.GITHUB.getBuilder("github")
                .clientId("a7553955a0c534ec5e6b")
                .clientSecret("1795b30b425ebb79e424afa51913f1c724da0dbb")
                .build();
    }

    public ClientRegistrationRepository clientRepository() {
    
    
        ClientRegistration c = clientRegistration();
        return new InMemoryClientRegistrationRepository(c);
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
    
    
        //设置认证方法
        http.oauth2Login(c -> {
    
    
                    c.clientRegistrationRepository(clientRepository());
                });

        //指定一个用户需要经过认证才能提出请求
        http.authorizeRequests()
                .anyRequest().authenticated();
    }
}

  • 方式2
package com.laurentiuspilca.ssia.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.config.oauth2.client.CommonOAuth2Provider;
import org.springframework.security.oauth2.client.registration.ClientRegistration;
import org.springframework.security.oauth2.client.registration.ClientRegistrationRepository;
import org.springframework.security.oauth2.client.registration.InMemoryClientRegistrationRepository;
import org.springframework.security.oauth2.core.AuthorizationGrantType;

@Configuration
public class ProjectConfig extends WebSecurityConfigurerAdapter {
    
    
    
    private ClientRegistration clientRegistration() {
    
    
        return CommonOAuth2Provider.GITHUB.getBuilder("github")
                .clientId("a7553955a0c534ec5e6b")
                .clientSecret("1795b30b425ebb79e424afa51913f1c724da0dbb")
                .build();
    }

    public ClientRegistrationRepository clientRepository() {
    
    
        var c = clientRegistration();
        return new InMemoryClientRegistrationRepository(c);
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
    
    
        http.oauth2Login();

        http.authorizeRequests()
                .anyRequest().authenticated();
    }
}

注意 一个配置选项和另一个一样好,但请记住我们在第二章中讨论的内容。为了使你的代码易于理解,避免混合配置方法。要么使用在上下文中用bean设置每一个东西的方法,要么使用代码内联配置风格。

12.5.5 Spring Boot配置的魅力

在这一节中,我将向你展示第三种方法,直接从属性文件中构建ClientRegistration和ClientRegistrationRepository对象。这种方法在Spring Boot项目中并不罕见。我们也可以看到这种情况发生在其他对象上。

例如,我们经常看到根据属性文件配置的数据源。下面的代码片断显示了如何在application.properties文件中为我们的例子设置client registration。

spring.security.oauth2.client.registration.github.client-id=a7553955a0c534ec5e6b
spring.security.oauth2.client.registration.github.client-secret=1795b30b425ebb79e424afa51913f1c724da0dbb

在这个片段中,我只需要指定客户端ID和客户端密码。因为提供者的名字是github,Spring Boot知道从CommonOAuth2Provider类中获取有关URI的所有细节。现在,我的配置类看起来就像下面代码清单。

@Configuration
public class ProjectConfig
        extends WebSecurityConfigurerAdapter {
    
    
    @Override
    protected void configure(HttpSecurity http) throws Exception {
    
    
        http.oauth2Login();
        http.authorizeRequests()
                .anyRequest()
                .authenticated();
    }
}

我们不需要指定关于ClientRegistration和ClientRegistrationRepository的任何细节,因为它们是由Spring Boot根据属性文件自动创建的。如果我们使用Spring Security已知的常见提供商以外的提供商,我们还需要使用以spring.security.oauth2.client.provider开头的属性组来指定授权服务器的细节。下面的代码片段为你提供了一个例子。

spring.security.oauth2.client.provider.myprovider.authorization-uri=<some uri>
spring.security.oauth2.client.provider.myprovider.token-uri=<some uri>

在我需要在内存中拥有一个或多个authentication providers的情况下,就像我们在当前的例子中所做的那样,我更喜欢按照我在本节中介绍的方式进行配置。它更干净,更容易管理。

但是如果我们需要一些不同的东西,比如将客户的注册信息存储在数据库中或者从网络服务中获取,那么我们就需要创建一个自定义的ClientRegistration-
Repository实现。在这种情况下,我们需要按照你在12.5.5节中学到的方法来设置它。

12.5.6 获取已验证用户的详细信息

在本节中,我们将讨论获取和使用认证用户的详细信息。你已经知道,在Spring Security架构中,是由SecurityContext来存储认证用户的详细信息。一旦认证过程结束,负责的过滤器会将认证对象存储在SecurityContext中。应用程序可以从那里获取用户的详细信息,并在需要时使用它们。同样的情况也发生在OAuth 2认证中。

在这种情况下,框架所使用的认证对象的实现被命名为OAuth2AuthenticationToken。你可以直接从SecurityContext中获取它,或者让Spring Boot在api的一个参数中为你注入它,正如你在第6章中学到的那样。下面的列表显示了我如何改变控制器来接收并在控制台中打印用户的详细信息。

代码清单12.9 使用已登录用户的详细信息

package com.hashnode.proj0001firstspringsecurity.controller;

import org.springframework.security.oauth2.client.authentication.OAuth2AuthenticationToken;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;

import java.util.logging.Logger;

@Controller
public class MainController {
    
    

    private Logger logger =
            Logger.getLogger(MainController.class.getName());

    @GetMapping("/")
    public String main(OAuth2AuthenticationToken token) {
    
    
        //Spring Boot自动在方法的参数中注入代表用户的Authentication对象。
        logger.info(String.valueOf(token));
        return "main.html";
    }
}

12.5.7 测试应用程序

在本节中,我们将测试本章中的应用程序。在检查功能的同时,我们遵循OAuth 2授权代码授予类型的步骤(图12.15),以确保你正确理解它,并观察Spring Security是如何通过我们的配置来应用它的。

image-20230129174233986

图12.15 该应用将GitHub作为授权服务器,同时也作为资源服务器。 当用户想要登录时,客户端会将用户重定向到GitHub的登录页面。当用户成功登录后,GitHub 会向我们的应用程序回拨一个授权码。我们的应用程序使用该授权码来请求一个访问token。然后,应用程序可以通过提供访问token从资源服务器(GitHub)访问用户详情。资源服务器的响应提供了用户的详细信息和主页面的URL。

我首先确定我没有登录到GitHub。我还确保我打开了一个浏览器来检查请求导航的历史。这个历史记录可以让我了解OAuth 2流程中发生的步骤,也就是我们在12.3.1节中讨论的步骤。如果我通过了认证,那么应用程序就会直接记录我。然后我启动应用程序,在浏览器中访问我们应用程序的主页面:

image-20230129174435444

应用程序将我重定向到以下代码片段中的URL(并在图12.16中预发)。这个URL被配置在GitHub的CommonOauth2Provider类中作为授权URL。

https://github.com/login?client_id=15a2c5a0e3fe921a223d&return_to=%2Flogin%2Foauth%2Fauthorize%3Fclient_id%3D15a2c5a0e3fe921a223d%26redirect_uri%3Dhttp%253A%252F%252Flocalhost%253A9090%252Flogin%252Foauth2%252Fcode%252Fgithub%26response_type%3Dcode%26scope%3Dread%253Auser%26state%3DSS5dh9rn0evuxXqBc_-wvgdlecYB8VpcTB-EgcGgtqg%253D

image-20230129175309685

图 12.16 进入主页面后,浏览器将我们重定向到 GitHub 登录。在 Chrome 的控制台工具中,我们可以看到对 localhost 的调用,然后是对 GitHub 授权api的调用。

我们的应用程序将所需的查询参数附加到URL上,正如我们在第12.3.1节所讨论的那样。这些参数是

  • response_type,其值为code
  • client_id
  • scope(值read:user也定义在CommonOauth2Provider类中)。
  • state值为CSRF令牌

image-20230129175825394

我们使用我们的GitHub凭证,用GitHub登录我们的应用程序。正如你在图12.17中看到的那样,我们通过了认证并被重定向回来。

image-20230129180759295

图 12.17 填写完凭证后,GitHub 将我们重定向到我们的应用程序。现在我们可以看到主页面,应用程序可以利用访问令牌从 GitHub 访问用户的详细信息。

下面的代码片断显示了GitHub给我们响应的URL。你可以看到,GitHub提供了授权码,我们的应用程序使用该授权码来请求获取token。

http://localhost:9090/login/oauth2/code/github?code=4ad786953479921c594d&state=yH8bDv81m9QsIRCT3urcU67forv-GSGuR3QulqAKH7Y=

可以看到返回的token如下:

image-20230129182005307

我们不会从浏览器中看到对token对象获取 api的调用,因为这直接发生在我们的应用程序中。但我们可以相信,应用程序设法获得了一个token对象,因为我们可以看到控制台中打印的用户详细信息。这意味着应用程序获取了token对象。接下来的代码片段向你展示了这个输出的一部分。

Name: [43921235],
Granted Authorities: [[ROLE_USER, SCOPE_read:user]], User Attributes:
[{
    
    login=lspil, id=43921235, node_id=MDQ6VXNlcjQzOTIxMjM1,
avatar_url=https://avatars3.githubusercontent.com/u/43921235?v=4,
gravatar_id=, url=https://api.github.com/users/lspil, html_url=https://
github.com/lspil, followers_url=https://api.github.com/users/lspil/
followers, following_url=https://api.github.com/users/lspil/following{
    
    /
other_user}, …

总结

  • OAuth 2框架描述了允许一个实体代表其他人访问资源的方法。我们在应用程序中使用它来实现认证和授权逻辑。
  • 一个应用程序可以用来获取访问令牌的不同流程被称为授予。根据系统结构的不同,你需要选择一个合适的授予类型。
    • 认证码授予类型的工作方式是允许用户直接在授权服务器上进行认证,这使得客户端可以获得一个访问令牌。当用户不信任客户端并且不想与之分享他们的凭证时,我们选择这种授予类型。
    • 密码授予类型意味着用户与客户端共享其凭证。只有在你能信任客户端的情况下,你才应该应用这个。
    • 客户端凭证授予类型意味着客户端只通过验证其凭证来获得令牌。当客户端需要调用资源服务器的一个不是用户资源的api时,我们选择这种授予类型。
  • Spring Security实现了OAuth 2框架,允许你用几行代码在你的应用程序中实现它。
  • 在Spring Security中,你可以使用ClientRegistration的一个实例来表示客户在授权服务器上的注册。
  • Spring Security OAuth 2实现中负责寻找特定客户端注册的组件被称为ClientRegistrationRepository。在用Spring Security实现OAuth 2客户端时,你需要定义一个至少有一个客户端注册的ClientRegistrationRepository对象。

猜你喜欢

转载自blog.csdn.net/Learning_xzj/article/details/128998649
今日推荐