15、Spring Security 速成 - OAuth2的运行机制(上)-OAuth2概念和授权码模式讲解

一、前言

鸽了很久,其实也因为自己确实比较忙,加之自己在造demo的时候也遇到了很多问题,并且网上这方面的解答非常之少,不过也正是因为少,才更加让我想写这样的知识分享,最终,在一篇博客的解答下解决了问题,本节我们就开始Oauth2的学习。在之前的学习中,我们可以说SpringSecurity本身的基础学习差不多告一段落,大家已经可以通过自己的能力去搭建一个SpringSecurity的安全应用。

那么接下来我们就接着学习OAuth2这样一个庞大的主题,我们在这一章主要介绍一个概要,即OAuth2是什么,然后将会把它应用到一个关注使用单点登录(SSO)进行身份验证的应用程序上。这里之所以选用SSO,是因为其非常简单,也非常有用。

二、OAuth2框架

在大多数情况下,OAuth2被称为授权框架(或规范框架),其主要目的是允许第三方网站或应用程序访问资源。有时人们也把OAuth2称为一项委托协议。无论如何称呼它,重要的是要记住OAuth2不是一个特定的实现或库。也可以将OAuth2流程定义应用于其他平台、工具或语言。这里将介绍如何实现OAuth2与SpringBoot和Spring Security的集成应用。

理解OAuth2我们可以通过和之前学习过的HTTP Basic身份验证方法进行对比,我们以前使用的是HTTP Basic身份言则会那个,它有以下两个问题:

  • 为每个请求发送凭据
  • 由单独的系统管理用户的凭据

为每个请求发送凭据可能只适用于隔离环境的情况,但这通常是不可取的,因为这意味着:

  • 需要经常在网络上共享凭据
  • 让客户端(就Web应用程序而言就是浏览器)以某种方式存储凭据,以便客户端可以将这些凭据发送到服务器,并请求进行身份验证和授权。

我们希望在应用程序的架构中去掉这两点,因为它们会使凭据变成漏洞,从而削弱安全性。通常,我们会希望有一个单独的系统管理系统用户凭据。假设我们必须为组织中使用的所有应用程序配置和使用单独的凭据。如下图:在组织中,通常会使用多个应用程序。其中大多数需要用户进行身份验证才能使用。用户需要记住多个密码,组织要管理多个凭据集合,这将是一项挑战。

 
如果将凭据管理的职责隔离在系统的一个组件中会更好。目前,我们将其称为授权服务器。如下图:它拥有更易于维护的架构会将凭据单独保存,并允许所有应用程序为其用户使用同一组凭据

 
这种方式消除了表示同一个人的重复凭据。通过这种方式,架构将变得简单。

三、OAuth2身份验证架构的组件

  • 资源服务器托管用户所拥有的资源的应用程序。资源可以是用户的数据或他们被授权的操作
  • 用户(也成为资源拥有者):拥有由资源服务器暴露的资源的个体。用户通常有一个用户名和密码,他们用用户名和密码标识自己。
  • 客户端:以用户名义访问用户所拥有的资源的应用程序客户端使用客户端ID和客户端密钥来标识自己。请注意,这些凭据与用户凭据不同。客户端在发出请求时需要自己的凭据来标识自己
  • 授权服务器授权客户端访问由资源服务器暴露的用户资源的应用程序当授权服务器决定授权客户端已用户名义访问资源时,它会发出一个令牌客户端使用此令牌向资源服务器证明他已被授权服务器授权。如果它有一个有效的令牌,则资源服务器将允许客户端访问它请求的资源

 

四、使用OAuth2的实现选项

OAuth2主要是指使用令牌进行授权。令牌就像门禁卡一样。一旦获得令牌,就可以访问特定的资源。但是OAuth2提供了多种可能性以便获取令牌也称为授权。以下是可以选择的最常见的OAuth2授权方式。

  • 授权码
  • 密码
  • 刷新令牌
  • 客户端凭据

4.1、实现授权码授权类型

这种授权类型是最常用的OAuth2流程之一。

 
如上图,授权代码授权类型。客户端会要求用户直接与授权服务器交互,以便为其授予用户请求的权限。在授权之后,授权服务器会发出令牌,这样客户端就可以使用该令牌访问用户的资源。

ps:上图中的箭头不一定表示HTTP请求和响应。这些箭头表示OAuth2的参与者之间交换的消息。例如,当客户端告知用户“告知授权服务器你允许我做这个操作”时,客户端就会将用户重定向到授权服务器登录页面。当授权服务器向客户端提供访问令牌时,授权服务器实际上会根据所谓的重定向URI来调用客户端。当然这些细节后面会讲到,这里只需知道这些序列图不仅仅表示HTTP请求和响应,它们是对OAuth2参与者之间通信的简化描述

以下是授权码授权类型的工作方式。接下来将详细深入讲解每个步骤的细节。

(1)发出身份验证请求

(2)获取访问令牌

(3)调用受保护的资源

步骤1:使用授权码授权类型发出身份验证请求

客户端将用户重定向到需要进行身份验证的授权服务器端点。假设我们正在使用应用程序X,并且需要访问一个受保护的资源。为了访问该资源,应用程序X需要我们进行身份验证。它会打开一个授权服务器上的页面,其中包含登录表单,我们必须用凭据填写该表单

ps:这里真正需要注意的是,用户会直接与授权服务器交互。用户不会向客户端应用程序发送凭据
从技术上讲,这里发生的处理是,当客户端将用户重定向到授权服务器时,客户端会调用授权端点,并在请求中使用以下详细信息。

  • 带有code值的response_type,它会告知授权服务器客户端需要一个授权码。客户端需要该授权码用来获取访问令牌,第(2)步中将会接受。
  • client_id具有客户端ID值,它会标识应用程序本身
  • redirect_uri它会告知授权服务器在成功进行身份验证之后将用户重定向到何处。有时授权服务器已经知道每个客户端的默认重定向URI。由于这个原因,客户端不需要发送重定向URI。
  • scope,它类似于被授予的权限
  • state,他定义了一个跨站请求伪造(CSRF)令牌。用以CSRF防护
    拿一个后面实例中这一步出现的链接说明:

 
这一步就是客户端将用户重定向到授权服务器的场景,此时客户端向授权服务器(这里是gitee)发送请求,请求链接为:

https://gitee.com/oauth/authorize?response_type=code&client_id=c70egngnghhn464541b580db179d027df1f3017797e053890edd&scope=user_info&state=x5dLt6OkIRluOTVN4UQXG4mds5tqJBokhAd140_nqog%3D&redirect_uri=http://localhost:9090/login/oauth2/code/gitee

大家对比一下就会发现和上面的元素是一样的
身份验证成功后,授权服务器将根据重定向URI回调客户端,并提供授权码和状态值。客户端要检查状态值是否与它在请求中发送的状态值相同,以确认不是其他人试图调用重定向URI。之后客户端会使用授权码获取第(2)步中所示的访问令牌。

步骤2:使用授权码授权类型获取访问令牌

为了允许用户访问资源,第(1)步所产生的授权码就是经过身份验证的用户的客户端证明。没错,这就是它被称为授权码类型的原因。现在客户端将使用该授权码调用授权服务器以获取令牌

PS:在第一步中,交互发生在用户和授权服务器之间。而在这个步骤中,交互是在客户端和授权服务器之间进行的

 
你可能会好奇,为什么流程需要对授权服务器进行两次调用并且得到两个不同的令牌——授权码和访问令牌。花点时间理解这一点:

  • 授权服务器生成第一个授权码作为用户直接与之交互的证明。客户端接收到此授权码,并且必须再次使用该授权码及其凭据进行身份验证,以获得访问令牌。
  • 客户端使用第二个令牌访问资源服务器上的资源

那么授权服务器为什么不直接返回访问令牌呢?OAuth2定义了一个被称为隐式授权类型的流程,授权服务器会在其中直接返回访问令牌。该隐式授权类型后面不会去进行讲解,因为不建议使用,而且如今大多数授权服务器都不允许使用。授权服务器将使用访问令牌直接调用重定向URI,而不会确保接收该令牌的确实是正确的客户端,这一简单事实会降低流程的安全性通过首先发送授权码,客户端必须再次使用其凭据来证明其身份,以便获得令牌。客户端会进行最后一次调用以获取访问令牌并在其中发送:

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

从技术上讲,客户端现在会向授权服务器发出请求。该请求包含以下详细信息:

  • code,这是步骤1中接收到的授权码。这将证明用户经过了身份验证。
  • client_id和client_secret,它们是客户端的凭据。
  • redirect_uri,它与步骤1中用于验证的重定向URI相同。
  • 具有authorization_code值得grant_type,它会标识所使用的的流程的类型。服务器可能支持多个流程,因此必须始终指定当前要执行哪个身份验证流程。

作为响应,服务器会返回一个access_token这个令牌是一个客户端可用来调用由资源服务器暴露的资源的值

步骤3:使用授权码授权类型调用受保护资源

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

我将接口所经历的步骤总结成下图:

 
客户端从进行第三方认证操作的起点,默认格式为{baseUrl}/oauth2/authorization/{clientRegistrationId},其中clientRegistrationId代表着一个第三方标识,可以是微信、支付宝等开放平台,这里为gitee。用户点击了这个请求后就开始了授权之旅。

也就有了客户端向客户重定向的第②步,然后第⑤步就是向授权服务器(gitee)去获取授权码,然后用户通过认证同意授权后,授权服务器将客户重定向到通过配置的发回授权码的redirectUri发回授权码,
用户此时通过这个Url将授权码发给了客户端,客户端通过该授权码通过⑩请求获取accessToken
那么更详细的,也是我参考过觉得不错的文章有下面2篇,大家可以去看一下:

https://blog.csdn.net/longlivechina008/article/details/125007457
https://www.cnblogs.com/felordcn/p/13952072.html

其中上图中Client客户端中的OAuthFilter相当于之前SpringSecurity流程中的AuthenticationManager->AuthenticationProvider->UserDetailsService层层返回UserDetails到Filter,然后Filter将UserDetails打包为Authentication然后放到SecurityContext安全上下文中供后续使用,然后结束filterChain到达。

授权码是使用频率非常高的一种授权类型,大家需要好好吸收,尤其是概念,后面我们还会通过授权码授权类型的一个实例-sso单点登录去让大家印象更加深刻。

授权码授权类型的最大优点是让用户可以允许客户端执行特定的操作,而不需要与客户端共享其凭据,不过,这种授权类型有一个缺点:如果有人截获授权码,会发生什么?当然,如之前所属,客户端需要使用其凭据进行身份验证。但是,如果客户端凭据也以某种方式被盗了呢?即使这种情况非常罕见,但是我们也可以认为它是这种授权类型的漏洞。要避免这个漏洞,就需要借助PKCE授权码授权类型所提供的更复杂的场景,大家有兴趣可以自行了解。