Spring Security in Action 第一、二章 第一个Spring Security项目的建立以及基本

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


本章包括

  • 使用Spring Security创建你的第一个项目
  • 使用认证和授权的基本角色设计简单的功能
  • 应用基本接口来理解这些角色之间的关系
  • 为主要职责编写你的实现 覆盖Spring Boot的默认配置

Spring Boot是作为Spring框架的应用开发的一个进化阶段出现的。与其说你需要编写所有的配置,不如说Spring Boot已经预设了一些配置,所以你可以只覆盖那些与你的实现不匹配的配置。我们也称这种方法为 “惯例–配置”。

在这种开发应用程序的方式存在之前,开发人员为他们必须创建的所有应用程序反复编写几十行代码。在过去,当我们以单体方式开发大多数架构时,这种情况不太明显。在单体架构下,你只需要在开始时写一次这些配置,之后就很少需要再碰它们。当面向服务的软件架构发展起来后,我们开始感受到为配置每个服务而需要编写的模板代码的痛苦。如果你觉得有趣,你可以看看Willie Wheeler与Joshua White合著的《Spring in Practice》(Manning,2013)中的第三章。这一章是一本较早的书,描述了用Spring 3编写一个Web应用程序。这样一来,你就会明白,为了一个小小的单页Web应用,你不得不写多少配置。这里是该章的链接:https://livebook.manning.com/book/spring-in-practice/chapter-3/

由于这个原因,随着最近应用程序的发展,特别是那些微服务的发展,Spring Boot变得越来越流行。Spring Boot为你的项目提供了自动配置功能,并缩短了设置所需的时间。我想说的是,它为今天的软件开发带来了合适的理念。
在本章中,我们将从第一个使用Spring Security的应用程序开始。对于你用Spring框架开发的应用,Spring Security是实现应用级安全的绝佳选择。我们将使用Spring Boot并讨论自动配置的默认值,以及对覆盖这些默认值的简要介绍。对默认配置的考虑为Spring Security提供了一个很好的介绍,同时也说明了认证的概念。
一旦我们开始了第一个项目,我们将更详细地讨论各种认证选项。在第3章到第6章中,我们将继续为你在第一个例子中看到的不同职责提供更具体的构架。你还会看到应用这些配置的不同方法,这取决于架构风格。在本章中,我们将采用以下步骤:

  1. 创建一个只有Spring Security和Web依赖的项目,看看如果你不添加任何配置,它的表现如何。这样,你就会明白你应该从默认的认证和授权配置中期待什么。

  2. 改变项目,通过覆盖默认值来增加用户管理的功能,定义自定义用户和密码。

  3. 3 在观察到应用程序默认对所有端点进行认证后,了解到这也是可以定制的。

  4. 4 对相同的配置应用不同的风格,以了解最佳实践。

2.1 从第一个项目开始

让我们创建第一个项目,这样我们就有东西可以在第一个例子中工作。这个项目是一个小型的Web应用,暴露了一个REST端点。你会看到Spring Security是如何不费吹灰之力,使用HTTP Basic认证来保护这个端点的。仅仅通过创建项目和添加正确的依赖项,SpringBoot就会应用默认的配置,包括在你启动应用程序时使用一个用户名和密码。

注意 你有多种选择来创建Spring Boot项目。一些开发环境提供了直接创建项目的可能性。如果你在创建Spring Boot项目时需要帮助,你可以找到附录中描述的几种方法。如需更多细节,我推荐Craig Walls的《Spring Boot in Action》(Manning,2016)。Spring Boot in Action》中的第2章准确地描述了用Spring Boot创建一个Web应用(https://livebook.manning.com/book/spring-boot-in-action/chapter-2/)。

本书中的例子参考了源代码。在每个例子中,我还指定了你需要添加到pom.xml文件中的依赖项。你可以,而且我建议你这样做,下载本书提供的项目和可用的源代码:https://www.manning.com/downloads/2105。如果你遇到困难,这些项目会帮助你。你也可以用这些项目来验证你的最终解决方案。

注意 本书中的例子不依赖于你选择的构建工具。你可以使用Maven或Gradle。但为了保持一致,我用Maven构建了所有的例子。

第一个项目也是最小的一个。如前所述,它是一个简单的应用,暴露了一个REST端点,你可以调用它,然后接收图2.1中描述的响应。这个项目足以让你学习使用Spring Security开发应用程序的第一步。它介绍了用于认证和授权的Spring Security架构的基础知识。

image-20230201122509045

图2.1 我们的第一个应用程序使用HTTP Basic来验证和授权用户对一个端点。该应用在一个定义的路径(/hello)上暴露了一个REST端点。对于一个成功的调用,响应会返回一个HTTP 200状态信息和一个主体。这个例子演示了Spring Security默认配置的认证和授权是如何工作的。

我们开始学习Spring Security,先创建一个空项目,并将其命名为sia-ch2-ex1。(你也可以在本书提供的项目中找到这个同名的例子)。你需要为我们的第一个项目编写的唯一依赖项是spring-boot-starter-web和spring-boot-starter-security,如清单2.1所示。创建项目后,确保将这些依赖关系添加到你的 pom.xml 文件中。做这个项目的主要目的是看看默认配置的应用程序与Spring Security的行为。我们还想了解哪些组件是这个默认配置的一部分,以及它们的用途。

清单2.1 我们的第一个网络应用的Spring Security依赖项

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

我们现在可以直接启动应用程序。Spring Boot会根据我们添加到项目中的依赖关系,为我们应用Spring上下文的默认配置。但是,如果我们没有至少一个安全的端点,我们就无法了解安全问题。让我们创建一个简单的端点并调用它,看看会发生什么。为此,我们在空项目中添加一个类,并将这个类命名为HelloController。为此,我们在Spring Boot项目的主命名空间的某个地方,将该类添加到一个叫做controllers的包中。

注意 Spring Boot只在包含有@SpringBootApplication注释的类的包(及其子包)中扫描组件。如果你在主包之外的Spring中用任何定型组件来注解类,你必须使用@ComponentScan注解明确地声明该位置。

在下面的列表中,HelloController类为我们的例子定义了一个REST控制器和一个REST端点。

清单2.2 HelloController类和一个REST端点

package com.laurentiuspilca.ssia.controllers;

import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class HelloController {
    
    

    @GetMapping("/hello")
    public String hello() {
    
    
        return "Hello!";
    }
}

@RestController注解在上下文中注册了Bean,并告诉Spring,应用程序将这个实例作为Web控制器使用。此外,该注解还指定应用程序必须从HTTP响应的主体中设置返回值。@GetMapping注解将/hello路径映射到实现的方法上。一旦你运行该应用程序,除了控制台中的其他行,你应该看到类似这样的东西。

Using generated security password: 72b1e251-2743-4e90-90b2-ba776d87a27c

This generated password is for development use only. Your security configuration must be updated before running your application in production.

每次你运行应用程序时,它都会生成一个新的密码,并在控制台中打印出这个密码,如前面的代码片断所示。你必须使用这个密码来调用任何带有HTTP Basic认证的应用程序的端点。首先,让我们尝试在不使用授权头的情况下调用端点: curl -v http://localhost:8080/hello

注意 在本书中,我们使用cURL来调用所有例子中的端点。我认为cURL是最易读的解决方案。但如果你愿意,你可以使用你选择的工具。例如,你可能希望有一个更舒适的图形界面。在这种情况下,Postman是一个很好的选择。如果你使用的操作系统没有安装这些工具,你很可能需要自己安装它们。

响应为

{
    
     "status":401, "error":"Unauthorized", "message":"Unauthorized", "path":"/hello" }

响应状态是HTTP 401 Unauthorized。我们预料到了这个结果,因为我们没有使用适当的凭证进行认证。默认情况下,Spring Security期望使用默认的用户名(user)和所提供的密码(在我的例子中,以93a01开头的密码)。让我们再试一次,但现在用适当的凭证。

curl -u user:72b1e251-2743-4e90-90b2-ba776d87a27c http://localhost:8080/hello

响应为

Hello!

注意 HTTP 401 未授权状态代码有点含糊不清。通常情况下,它被用来表示认证失败而不是授权。开发人员在设计应用程序时,将其用于缺失或不正确的凭证等情况。对于一个失败的授权,我们可能会使用403禁止状态。一般来说,HTTP 403意味着服务器识别了请求的调用者,但他们没有所需的权限来完成他们试图进行的调用。

一旦我们发送了正确的凭证,你就可以在响应的正文中看到我们之前定义的HelloController方法所返回的内容。

用HTTP Basic认证调用端点
使用cURL,你可以用-u标志来设置HTTP基本用户名和密码。在幕后,cURL用Base64编码字符串:,并将其作为授权头的值发送,前缀为Basic字符串。而对于cURL来说,使用-u标志可能更容易。但是知道真正的请求是什么样子的也很重要。所以,让我们试一试,手动创建授权头。
第一步,取<用户名>:<密码>字符串,用Base64编码。当我们的应用程序进行调用时,我们需要知道如何形成授权头的正确值。你可以使用Linux控制台中的Base64工具来做这件事。你也可以找到一个用Base64编码字符串的网页,如https://www.base64encode.org。这个片段显示了Linux或Git Bash控制台中的命令:

echo -n user:93a01cf0-794b-4b98-86ef-54860f36f7f3 | base64

运行这个命令会返回这个Base64编码的字符串:

dXNlcjo5M2EwMWNmMC03OTRiLTRiOTgtODZlZi01NDg2MGYzNmY3ZjM=

现在你可以使用Base64编码的值作为调用的授权头的值。这个调用应该产生与使用-u选项的结果相同:

curl -H “Authorization: Basic dXNlcjo5M2EwMWNmMC03OTRiLTRiOTgtODZlZi01 ?NDg2MGYzNmY3ZjM=” localhost:8080/hello

结果为:

Hello!

对于默认项目,没有什么重要的安全配置需要讨论。我们主要使用默认配置来证明正确的依赖关系已经到位。它对认证和授权没有什么作用。这种实现并不是我们想在一个可生产的应用程序中看到的东西。但默认项目是一个很好的例子,你可以用它作为一个开始。
有了这个第一个例子,至少我们知道Spring Security已经到位了。下一步是改变配置,使这些配置适用于我们项目的要求。首先,我们将深入了解Spring Boot在Spring Security方面的配置,然后看看我们如何能够覆盖这些配置。

2.2 哪些是默认配置?

在本节中,我们将讨论整个架构中参与认证和授权过程的主要角色。你需要了解这方面的情况,因为你必须覆盖这些预配置的组件,以适应你的应用程序的需要。我将首先描述Spring Security的认证和授权架构是如何工作的,然后我们将把它应用到本章的项目中。如果一下子就讨论这些,那就太多了,所以为了尽量减少你在本章中的学习努力,我将讨论每个组件的高级情况。在接下来的章节中,你会了解到每个组件的细节。
在第2.1节中,你看到了一些执行认证和授权的逻辑。我们有一个默认的用户,每次启动应用程序时我们都会得到一个随机的密码。我们能够使用这个默认用户和密码来调用一个端点。但是,所有这些逻辑是在哪里实现的呢?你可能已经知道,Spring Boot为你设置了一些组件,这取决于你使用了哪些依赖关系。
在图2.2中,你可以看到Spring Security架构中的主要角色的大图片以及这些之间的关系。这些组件在第一个项目中都有一个预设的实现。在本章中,我让你了解Spring Boot在你的应用程序中配置了哪些Spring Security。我们还将讨论作为认证流程一部分的实体之间的关系。

image-20230201151053965

图2.2 在Spring Security的认证过程中起作用的主要组件以及它们之间的关系。这个架构代表了用Spring Security实现认证的骨干。在本书中,当我们讨论不同的认证和授权的实现时,我们会经常提到它。

在图2.2中,你可以看到

  • 认证过滤器将认证请求委托给认证管理器,并根据响应,配置安全上下文。
  • 认证管理器使用认证提供者来处理认证。
  • 认证提供者实现了认证逻辑。
  • 用户详情服务实现了用户管理职责,认证提供者在认证逻辑中使用了这些职责。
  • 密码编码器实现密码管理,认证提供者在认证逻辑中使用它。
  • 安全上下文在认证过程后保留认证数据。

在下面的段落中,我将讨论这些自动配置的bean:

  • UserDetailsService
  • PasswordEncoder

你也可以在图2.2中看到这些。认证提供者使用这些bean来寻找用户并检查他们的密码。让我们从提供认证所需凭证的方式开始。

一个实现了Spring Security的UserDetailsService接口的对象管理着用户的详细信息。到目前为止,我们使用的是Spring Boot提供的默认实现。这个实现只在应用程序的内部内存中注册了默认凭证。这些默认凭证是 “用户”,有一个默认的密码,是一个普遍的唯一标识符(UUID)。这个密码是在加载Spring上下文时自动生成的。这时,应用程序会将密码写入控制台,你可以看到它。因此,你可以在本章中我们刚刚研究的例子中使用它。

这个默认的实现只是作为一个概念证明,让我们看到依赖关系已经到位。该实现将凭证存储在内存中–应用程序并不持久保存凭证。这种方法适用于示例或概念验证,但在生产就绪的应用程序中应该避免使用。

然后,我们有了PasswordEncoder。PasswordEncoder做两件事:

  • 对password进行编码
  • 验证password是否与现有的编码相匹配

即使它不像UserDetailsService对象那样明显,PasswordEncoder对于Basic认证流程也是必须的。最简单的实现是以纯文本管理密码,而不对这些密码进行编码。我们将在第四章讨论这个对象的实现的更多细节。现在,你应该知道,PasswordEncoder和默认的UserDetailsService一起存在。当我们替换UserDetailsService的默认实现时,我们也必须指定一个PasswordEncoder。

Spring Boot在配置默认值时也选择了一种认证方法,即HTTP Basic访问认证。这是最直接的访问认证方法。基本认证只要求客户端通过HTTP授权头发送一个用户名和一个密码。在头的值中,客户端附加前缀Basic,然后是包含用户名和密码的字符串的Base64编码,用冒号(:)分开。

注意HTTP基本认证并不提供证书的保密性。Base64只是一种方便传输的编码方法;它不是一种加密或散列方法。在传输过程中,如果被拦截,任何人都可以看到凭证。一般来说,我们不使用HTTP基本认证,至少不使用HTTPS的保密性。你可以在RFC 7617(https://tools.ietf.org/html/rfc7617)中阅读HTTP Basic的详细定义。

AuthenticationProvider定义了认证逻辑,委托用户和密码管理。AuthenticationProvider的默认实现使用为UserDetailsService和PasswordEncoder提供的默认实现。隐含的是,你的应用程序保证了所有端点的安全。因此,对于我们的例子来说,我们唯一需要做的就是添加端点。另外,只有一个用户可以访问任何一个端点,所以我们可以说,在这种情况下,没有太多的授权需要做。

HTTP vs. HTTPS

你可能已经注意到,在我介绍的例子中,我只使用了HTTP。然而,在实践中,你的应用程序只能通过HTTPS进行通信。对于我们在本书中讨论的例子,无论我们使用HTTP还是HTTPS,与Spring Security相关的配置都没有什么不同。为了让你能专注于与Spring Security相关的例子,我不会为例子中的端点配置HTTPS。但是,如果你愿意,你可以为任何一个端点启用HTTPS,正如本侧边栏所介绍的那样。

有几种模式可以在系统中配置HTTPS。在某些情况下,开发人员在应用层面配置HTTPS;在其他情况下,他们可能使用服务网,或者他们可以选择在基础设施层面设置HTTPS。使用Spring Boot,你可以很容易地在应用层面上启用HTTPS,正如你将在本侧边栏的下一个例子中了解到的那样。

在任何这些配置场景中,你都需要一个由认证机构(CA)签署的证书。使用这个证书,调用端点的客户端就知道响应是否来自于认证服务器,并且没有人截获了通信。你可以购买这样的证书,但你必须更新它。如果你只需要配置HTTPS来测试你的应用程序,你可以使用像OpenSSL这样的工具生成一个自签名的证书。让我们来生成我们的自签名证书,然后在项目中配置它。

openssl req -newkey rsa:2048 -x509 -keyout key.pem -out cert.pem -days 365

在终端运行openssl命令后,你会被要求提供密码和关于你的CA的细节。因为这只是一个测试用的自签名证书,你可以在那里输入任何数据;只是要确保记住密码。该命令输出两个文件:key.pem(私钥)和cert.pem(公共证书)。我们将进一步使用这些文件来生成我们的自签名证书以启用HTTPS。在大多数情况下,证书是公钥密码学标准#12(PKCS12)。较少情况下,我们使用Java KeyStore(JKS)格式。让我们用PKCS12格式继续我们的例子。关于密码学的出色讨论,我推荐David Wong的《真实世界密码学》(Manning, 2020)。

openssl pkcs12 -export -in cert.pem -inkey key.pem -out certificate.p12 -name “certificate”

我们使用的第二个命令接收第一个命令生成的两个文件作为输入,并输出自签名证书。注意,如果你在Windows系统的Bash shell中运行这些命令,你可能需要在它前面加上winpty,如下面的代码片断所示。

winpty openssl req -newkey rsa:2048 -x509 -keyout key.pem -out cert.pem -days 365

winpty openssl pkcs12 -export -in cert.pem -inkey key.pem -out certificate.p12 -name “certificate”

最后,有了自签名的证书,你就可以为你的端点配置HTTPS了。将certificate.p12文件复制到Spring Boot项目的资源文件夹中,并在application.properties文件中添加以下几行。

server.ssl.key-store-type=PKCS12

server.ssl.key-store=classpath:certificate.p12

server.ssl.key-store-password=12345 #密码的值是你在运行第二条命令以生成PKCS12证书文件时指定的。

密码(在我这里是 “12345”)是在运行生成证书的命令后的提示中要求的。这就是为什么你在命令中没有看到它的原因。现在,让我们给我们的应用程序添加一个测试端点,然后用HTTPS调用它。

@RestController public class HelloController {
     
      
 @GetMapping("/hello") 
 public String hello() {
     
      
     return "Hello!";
 } 
}

如果你使用自签名的证书,你应该配置你用来进行终端调用的工具,使其跳过测试证书的真实性。如果工具测试了证书的真实性,它将无法识别它的真实性,并且调用将无法工作。在cURL中,你可以使用-k选项来跳过对证书真实性的测试。

curl -k https://localhost:8080/hello!

响应为:

Hello!

请记住,即使你使用HTTPS,你的系统组件之间的通信也不是完全安全的的。很多时候,我听到人们说,“我不再加密了,我使用HTTPS!” 虽然对保护通信有帮助,但HTTPS只是系统安全墙的其中一块砖。始终以负责任的态度对待你的系统的安全,并照顾到其中涉及的所有层次。

2.3 覆盖默认配置

现在你已经知道了第一个项目的默认值,现在是时候看看你如何替换这些默认值了。你需要了解你所拥有的覆盖默认组件的选项,因为这是你插入你的自定义实现和应用安全的方式,因为它适合你的应用程序。而且,正如你将在本节中所学习的,开发过程也是关于你如何编写配置以保持你的应用程序的高度可维护性。在我们要做的项目中,你经常会发现有多种方法可以覆盖一个配置。这种灵活性会造成混乱。我经常看到,在同一个应用程序中,不同风格的配置混合在一起,这是不可取的,Spring Security的不同部分。因此,这种灵活性也是一种警告。你需要学习如何选择,所以这一节也是关于了解你的选择是什么。

在某些情况下,开发者选择使用Spring上下文中的Bean进行配置。在其他情况下,他们会重写各种方法来达到同样的目的。Spring生态系统的发展速度可能是产生这些多种方法的主要因素之一。用混合的风格配置一个项目是不可取的,因为它使代码难以理解,并影响到应用程序的可维护性。了解你的选项以及如何使用它们是一项有价值的技能,它可以帮助你更好地理解你应该如何在项目中配置应用级安全。

在本节中,你将学习如何配置UserDetailsService和PasswordEncoder。这两个组件参与了身份验证,大多数应用程序根据他们的要求定制它们。虽然我们将在第3章和第4章中讨论关于定制它们的细节,但看到如何插入一个定制的实现是非常重要的。我们在本章中使用的实现都是由Spring Security提供的。

2.3.1 重写UserDetailsService组件

本章中我们谈到的第一个组件是UserDetailsService。正如你所看到的,应用程序在认证过程中使用这个组件。在本节中,你将学习如何定义一个UserDetailsService类型的自定义Bean。我们这样做是为了覆盖Spring Security提供的默认类型。正如你将在第3章中看到的那样,你可以选择创建你自己的实现或使用Spring Security提供的预定义的实现。在本章中,我们不打算详细介绍Spring Security提供的实现,也不打算创建我们自己的实现。我将使用由Spring Security提供的一个实现,名为InMemoryUserDetailsManager。通过这个例子,你将学会如何将这种对象插入你的架构中。

注意 Java中的接口定义了对象之间的契约。在应用程序的类设计中,我们使用接口来解耦相互使用的对象。在本书中讨论这些接口时,为了执行这一接口特性,我主要把它们称为契约。

为了向你展示用我们选择的实现来覆盖这个组件的方法,我们将改变我们在第一个例子中的做法。这样做可以让我们有自己的管理认证凭证。在这个例子中,我们没有实现我们的类,而是使用Spring Security提供的一个实现。

在这个例子中,我们使用InMemoryUserDetailsManager实现。即使这个实现比UserDetailsService多一点,现在我们也只从UserDetailsService的角度来参考它。这个实现在内存中存储凭证,然后可以被Spring Security用来验证请求。

注意 InMemoryUserDetailsManager的实现并不是为生产就绪的应用程序准备的,但它是一个很好的例子或概念证明的工具。在某些情况下,你所需要的只是用户。你不需要花时间去实现这部分的功能。在我们的案例中,我们用它来了解如何覆盖默认的UserDetailsService实现。

我们首先定义一个配置类。一般来说,我们在一个单独的名为config的包中声明配置类。清单2.3显示了配置类的定义。你也可以在项目sia-ch2-ex2中找到这个例子。

注意 本书中的例子是为Java 11设计的,它是最新的长期支持的Java版本。由于这个原因,我预计越来越多的生产中的应用程序将使用Java 11。所以在本书的例子中使用这个版本是非常有意义的。

你有时会看到我在代码中使用var。Java 10引入了保留类型名var,你只能在本地声明中使用它。在本书中,我用它来使语法更简短,同时也是为了隐藏变量类型。我们将在后面的章节中讨论var所隐藏的类型,所以你不必担心这个类型,直到要正确分析它的时候。

清单2.3 UserDetailsService Bean的配置类

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.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.password.NoOpPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.provisioning.InMemoryUserDetailsManager;

//@Configuration注解标志着该类是一个配置类。
@Configuration
public class ProjectConfig extends WebSecurityConfigurerAdapter {
    
    

    //@Bean注解指示Spring将返回的值作为一个Bean添加到Spring上下文中。
    @Override
    @Bean
    public UserDetailsService userDetailsService() {
    
    
        //var词使语法更短,并隐藏了一些细节。
        var userDetailsService = new InMemoryUserDetailsManager();

        return userDetailsService;
    }
}

我们用@Configuration来注解这个类。@Bean注解指示Spring将该方法返回的实例添加到Spring上下文中。如果你完全按照现在的样子执行代码,你就不会再在控制台中看到自动生成的密码了。应用程序现在使用你添加到上下文中的UserDetailsService类型的实例,而不是默认的自动配置的实例。但是,与此同时,由于两个原因,你将无法再访问这个端点:

  • 你没有任何用户。
  • 你没有一个PasswordEncoder。

在图2.2中,你可以看到认证也依赖于PasswordEncoder。让我们一步一步地解决这两个问题。我们需要:

  • 创建至少一个拥有一套证书(用户名和密码)的用户
  • 添加该用户,由我们的UserDetailsService的实现来管理
  • 定义一个PasswordEncoder类型的bean,我们的应用程序可以用它来验证一个给定的密码和UserDetailsService所存储和管理的密码。

首先,我们声明并添加一组凭证,我们可以用它来验证InMemoryUserDetailsManager的实例。在第三章,我们将讨论更多关于用户和如何管理他们的问题。目前,让我们使用一个预定义的构建器来创建一个UserDetails类型的对象。

当建立这个实例时,我们必须提供用户名、密码和至少一个权限。权限是一个允许该用户使用的动作,我们可以使用任何字符串。在清单2.4中,我把权限命名为read,但是因为我们暂时不会使用这个权限,这个名字其实并不重要。

清单2.4 用UserDetailsService的User builder类创建一个用户

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.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.password.NoOpPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.provisioning.InMemoryUserDetailsManager;

@Configuration
public class ProjectConfig extends WebSecurityConfigurerAdapter {
    
    

    @Override
    @Bean
    public UserDetailsService userDetailsService() {
    
    
        var userDetailsService = new InMemoryUserDetailsManager();

        //用给定的用户名、密码和权限列表建立用户
        var user = User.withUsername("john")
                .password("12345")
                .authorities("read")
                .build();

        //添加要由UserDetailsService管理的用户。
        userDetailsService.createUser(user);

        return userDetailsService;
    }

    
}

注意 你可以在org.springframework.security.core .userdetails包中找到User这个类。它是我们用来创建代表用户的对象的构建器实现。另外,作为本书的一般规则,如果我没有在代码列表中介绍如何编写一个类,那就意味着Spring Security提供了它。

正如清单2.4所介绍的,我们必须为用户名提供一个值,为密码提供一个值,并且至少为权限提供一个值。但这仍然不足以让我们调用端点。我们还需要声明一个PasswordEncoder。

当使用默认的UserDetailsService时,一个PasswordEncoder也是自动配置的。因为我们覆盖了UserDetailsService,所以我们也必须声明一个PasswordEncoder。现在尝试一下这个例子,当你调用端点时,你会看到一个异常。当试图进行认证时,Spring Security意识到它不知道如何管理密码,因此失败了。异常看起来就像下一个代码片断中的那样,你应该在你的应用程序的控制台中看到它。客户端得到一个HTTP 401未授权的消息和一个空的响应体。

curl -u john:12345 http://localhost:8080/hello

响应为

在这里插入图片描述

为了解决这个问题,我们可以在上下文中添加一个PasswordEncoder Bean,就像我们对UserDetailsService所做的一样。对于这个Bean,我们使用一个现有的PasswordEncoder的实现。

注意 NoOpPasswordEncoder实例将密码视为纯文本。它不对它们进行加密或散列。对于匹配,NoOpPasswordEncoder只使用String类的底层equals(Object o)方法对字符串进行比较。你不应该在准备好的应用程序中使用这种类型的PasswordEncoder。NoOpPasswordEncoder是一个很好的选择,对于那些你不想关注密码的散列算法的例子。因此,该类的开发者将其标记为@Deprecated,你的开发环境将以删除线显示其名称。

你可以在以下列表中看到配置类的完整代码。

清单2.5 配置类的完整定义

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.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.password.NoOpPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.provisioning.InMemoryUserDetailsManager;

@Configuration
public class ProjectConfig extends WebSecurityConfigurerAdapter {
    
    

    @Override
    @Bean
    public UserDetailsService userDetailsService() {
    
    
        var userDetailsService = new InMemoryUserDetailsManager();

        var user = User.withUsername("john")
                .password("12345")
                .authorities("read")
                .build();

        userDetailsService.createUser(user);

        return userDetailsService;
    }

    @Bean
    public PasswordEncoder passwordEncoder() {
    
    
        return NoOpPasswordEncoder.getInstance();
    }

}

让我们用新的用户John和密码12345试试这个端点。

curl -u john:12345 http://localhost:8080/hello
Hello!

注意 知道了单元和集成测试的重要性,你们中的一些人可能已经想知道为什么我们不给我们的例子写测试。实际上,你可以在本书提供的所有例子中找到相关的Spring Security集成测试。然而,为了帮助你专注于每一章的主题,我将有关测试Spring Security集成的讨论分开,并在第20章中详细介绍。

2.3.2 覆盖端点授权配置

如2.3.1节所述,在对用户进行新的管理后,我们现在可以讨论端点的认证方法和配置。在第7、8、9章中你会学到很多关于授权配置的东西。但在深入研究细节之前,你必须了解大局。而实现这一目标的最好方法就是我们的第一个例子。在默认配置下,所有的端点都假定你有一个由应用程序管理的有效用户。另外,默认情况下,你的应用程序使用HTTP Basic认证作为授权方法,但你可以很容易地覆盖这个配置。

正如你在接下来的章节中所了解的,HTTP Basic认证并不适合大多数应用程序的架构。有时我们想改变它以适应我们的应用。同样地,并不是所有的应用程序的端点都需要安全,对于那些需要安全的端点,我们可能需要选择不同的授权规则。为了做出这样的改变,我们首先要扩展WebSecurityConfigurerAdapter类。扩展这个类允许我们覆盖configure(HttpSecurity http)方法,正如下一个列表中所介绍的。对于这个例子,我将继续在项目sia-ch2-ex2中编写代码。

清单2.6 扩展WebSecurityConfigurerAdapter

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.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.password.NoOpPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.provisioning.InMemoryUserDetailsManager;

@Configuration
public class ProjectConfig extends WebSecurityConfigurerAdapter {
    
    

    //Omitted code

    @Override
    protected void configure(HttpSecurity http) throws Exception {
    
    
        // ...
    }
}

然后我们可以使用HttpSecurity对象的不同方法来改变配置,如下面的列表所示。

清单2.7 使用HttpSecurity参数来改变配置

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.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.password.NoOpPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.provisioning.InMemoryUserDetailsManager;

@Configuration
public class ProjectConfig extends WebSecurityConfigurerAdapter {
    
    

    //Omitted code

    @Override
    protected void configure(HttpSecurity http) throws Exception {
    
    
        http.httpBasic();
        //所有的请求都需要认证。
        http.authorizeRequests().anyRequest().authenticated();
    }
}

列表2.7中的代码配置了端点授权,其行为与默认的相同。你可以再次调用端点,看看它的行为是否与前面2.3.1节的测试相同。只要稍作改动,你就可以让所有的端点在不需要凭证的情况下被访问。你会在下面的列表中看到如何做到这一点。

清单 2.8 使用 permitAll() 改变授权配置

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.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.password.NoOpPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.provisioning.InMemoryUserDetailsManager;

@Configuration
public class ProjectConfig extends WebSecurityConfigurerAdapter {
    
    

    //Omitted code

    @Override
    protected void configure(HttpSecurity http) throws Exception {
    
    
        http.httpBasic();
        //所有的请求都不需要认证。
        http.authorizeRequests().anyRequest().permitAll();
    }
}

现在,我们可以在不需要证书的情况下调用/hello端点。配置中的 permitAll() 调用,加上 anyRequest() 方法,使得所有的端点都可以在不需要凭据的情况下被访问。

curl http://localhost:8080/hello
Hello!

这个例子的目的是让你感受一下如何覆盖默认配置。我们将在第7章和第8章讨论有关授权的细节。

2.3.3 以不同方式设置配置

使用Spring Security创建配置的一个令人困惑的方面是有多种方法来配置同一件事。在本节中,你将学习配置UserDetailsService和PasswordEncoder的其他方法。了解你所拥有的选项是非常重要的,这样你就可以在本书或其他来源(如博客和文章)中找到的例子中识别这些选项。同样重要的是,你要了解如何以及何时在你的应用程序中使用这些。在接下来的章节中,你会看到不同的例子,这些例子扩展了本节中的信息。

让我们来看看第一个项目。在我们创建了一个默认的应用程序后,我们通过在Spring上下文中添加新的实现作为Bean,成功地覆盖了UserDetailsService和PasswordEncoder。让我们找到另一种方法来为UserDetailsService和PasswordEncoder做同样的配置。

在配置类中,我们不把这两个对象定义为Bean,而是通过configure(AuthenticationManagerBuilder auth)方法对其进行设置。我们从WebSecurityConfigurerAdapter类中覆盖这个方法,并使用其AuthenticationManagerBuilder类型的参数来设置UserDetailsService和PasswordEncoder,如下面列表所示。你可以在项目sia-ch2-ex3中找到这个例子。

清单2.9 在configure()中设置UserDetailsService和PasswordEncoder。

package com.laurentiuspilca.ssia.config;

import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.crypto.password.NoOpPasswordEncoder;
import org.springframework.security.provisioning.InMemoryUserDetailsManager;

@Configuration
public class ProjectConfig extends WebSecurityConfigurerAdapter {
    
    

    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
    
    
          //声明一个UserDetailsSevice,在内存中存储用户。
        var userDetailsService = new InMemoryUserDetailsManager();

      
        //定义了一个用户及其所有细节
        var user = User.withUsername("john")
                .password("12345")
                .authorities("read")
                .build();

        //添加用户,由我们的UserDetailsSevice管理。
        userDetailsService.createUser(user);

        //UserDetailsService和PasswordEncoder现在是在configure()方法中设置的。
        auth.userDetailsService(userDetailsService)
            .passwordEncoder(NoOpPasswordEncoder.getInstance());
    }
}

在清单2.9中,你可以看到我们以与清单2.5相同的方式声明了UserDetailsService。不同的是,现在这是在第二个重载方法中本地完成的。我们还从AuthenticationManagerBuilder中调用userDetailsService()方法来注册UserDetailsService实例。此外,我们调用 passwordEncoder() 方法来注册 PasswordEncoder。清单2.10显示了配置类的全部内容。

注意 WebSecurityConfigurerAdapter类包含三个不同的重载configure()方法。在清单2.9中,我们重载了一个与清单2.8不同的方法。在接下来的章节中,我们将更详细地讨论这三个方法。
这些配置选项中的任何一个都是正确的。第一个选项,即我们将bean添加到上下文中,可以让你在另一个可能需要它们的类中注入这些值。但如果你的情况不需要这样,第二个选项也同样不错。然而,我建议你避免混合配置,因为这可能会造成混乱。例如,下面列表中的代码会让你怀疑UserDetailsService和PasswordEncoder之间的联系在哪里。

清单2.10 配置类的完整定义

package com.laurentiuspilca.ssia.config;

import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.crypto.password.NoOpPasswordEncoder;
import org.springframework.security.provisioning.InMemoryUserDetailsManager;

@Configuration
public class ProjectConfig extends WebSecurityConfigurerAdapter {
    
    

    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
    
    
          //声明一个UserDetailsSevice,在内存中存储用户。
        var userDetailsService = new InMemoryUserDetailsManager();

      
        //定义了一个用户及其所有细节
        var user = User.withUsername("john")
                .password("12345")
                .authorities("read")
                .build();

        //添加用户,由我们的UserDetailsSevice管理。
        userDetailsService.createUser(user);

        //UserDetailsService和PasswordEncoder现在是在configure()方法中设置的。
        auth.userDetailsService(userDetailsService)
            .passwordEncoder(NoOpPasswordEncoder.getInstance());
    }
    
     @Override
    protected void configure(HttpSecurity http) throws Exception {
    
    
        http.httpBasic();
        //指定所有请求都需要认证
        http.authorizeRequests().anyRequest().authenticated();
    }
}

这些配置选项中的任何一个都是正确的。第一个选项,即我们将bean添加到上下文中,可以让你在另一个你可能需要它们的类中注入这些值。但如果你的情况不需要这样,第二个选项也同样不错。然而,我建议你避免混合配置,因为这可能会造成混乱。

使用AuthenticationManagerBuilder,你可以直接配置用户进行认证。在这种情况下,它会为你创建UserDetailsService。然而,语法变得更加复杂,可以说是难以阅读。我不止一次看到这种选择,即使是在生产就绪的系统中。

可能是这个例子看起来很好,因为我们使用内存方式来配置用户。但在一个生产应用中,情况并非如此。在那里,你很可能把你的用户存储在一个数据库中,或者从另一个系统中访问他们。在这种情况下,配置可能会变得非常长和难看。清单2.12显示了你可以为内存中的用户编写配置的方法。你可以在项目sia-ch2-ex4中找到这个例子。

清单2.12 配置内存中的用户管理

package com.laurentiuspilca.ssia.config;

import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.crypto.password.NoOpPasswordEncoder;

@Configuration
public class ProjectConfig extends WebSecurityConfigurerAdapter {
    
    

    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
    
    
        auth.inMemoryAuthentication()
                .withUser("john")
                .password("12345")
                .authorities("read")
        .and()
            .passwordEncoder(NoOpPasswordEncoder.getInstance());
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
    
    
        http.httpBasic();
        http.authorizeRequests().anyRequest().authenticated();
    }
}

一般来说,我不推荐这种方法,因为我发现在一个应用程序中,最好是尽可能地分离和编写责任的解耦。

2.3.4 重写AuthenticationProvider的实现

正如你已经观察到的,Spring Security组件提供了很大的灵活性,这为我们在将这些组件适应于我们的应用程序的架构时提供了很多选择。到目前为止,你已经了解了UserDetailsService和PasswordEncoder在Spring Security架构中的作用。你也看到了一些配置它们的方法。现在是时候了,你也可以定制委托给这些的组件,即AuthenticationProvider。

image-20230201161532018

图2.3 AuthenticationProvider实现了认证逻辑。它接收来自AuthenticationManager的请求,并委托UserDetailsService寻找用户,并将密码的验证交给PasswordEncoder。

图2.3显示了AuthenticationProvider,它实现了认证逻辑并委托给UserDetailsService和PasswordEncoder来管理用户和密码。所以我们可以说,通过这一节,我们在认证和授权架构中更深入一步,学习如何用AuthenticationProvider实现自定义认证逻辑。

因为这是第一个例子,我只向你展示了简单的图片,以便你更好地理解架构中各组件之间的关系。但我们会在第3、4、5章中详细介绍。在这些章节中,你会发现AuthenticationProvider的实现,以及在一个更重要的练习中,即本书的第一个 "动手 "部分,第6章。

我建议你尊重Spring Security架构中设计的职责。这个架构是松散耦合的,有细粒度的责任。这种设计是使Spring Security灵活并易于集成到你的应用程序中的原因之一。但是,根据你如何利用其灵活性,你也可以改变设计。你必须谨慎对待这些方法,因为它们会使你的解决方案复杂化。例如,你可以选择覆盖默认的AuthenticationProvider,这样你就不再需要UserDetailsService或PasswordEncoder了。考虑到这一点,下面的列表显示了如何创建一个自定义的认证提供者。你可以在项目sia-ch2-ex5中找到这个例子。

清单2.13 实现AuthenticationProvider接口

package com.laurentiuspilca.ssia.security;

import org.springframework.security.authentication.AuthenticationCredentialsNotFoundException;
import org.springframework.security.authentication.AuthenticationProvider;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.stereotype.Component;

import java.util.Arrays;

@Component
public class CustomAuthenticationProvider implements AuthenticationProvider {
    
    

    @Override
    public Authentication authenticate(Authentication authentication) throws AuthenticationException {
    
    
        //这里的认证逻辑
        String username = authentication.getName();
        String password = String.valueOf(authentication.getCredentials());

        if ("john".equals(username) && "12345".equals(password)) {
    
    
            return new UsernamePasswordAuthenticationToken(username, password, Arrays.asList());
        } else {
    
    
            throw new AuthenticationCredentialsNotFoundException("Error!");
        }
    }

    @Override
    public boolean supports(Class<?> authenticationType) {
    
    
        //这里是认证实现的类型
        return UsernamePasswordAuthenticationToken.class.isAssignableFrom(authenticationType);
    }
}

authenticate(Authentication认证)方法代表了认证的所有逻辑,所以我们将在清单2.14中添加这样的实现。我将在第5章中详细解释supports()方法的用法。目前,我建议你把它的实现视为理所当然。对于当前的例子来说,它并不是必不可少的。

清单2.14 实现认证逻辑

package com.laurentiuspilca.ssia.security;

import org.springframework.security.authentication.AuthenticationCredentialsNotFoundException;
import org.springframework.security.authentication.AuthenticationProvider;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.stereotype.Component;

import java.util.Arrays;

@Component
public class CustomAuthenticationProvider implements AuthenticationProvider {
    
    

    @Override
    public Authentication authenticate(Authentication authentication) throws AuthenticationException {
    
    
        //getName()方法是由Authentication从Principal接口继承的。
        String username = authentication.getName();
        String password = String.valueOf(authentication.getCredentials());

        //这个条件一般调用UserDetailsService和PasswordEncoder来测试用户名和密码。
        if ("john".equals(username) && "12345".equals(password)) {
    
    
            return new UsernamePasswordAuthenticationToken(username, password, Arrays.asList());
        } else {
    
    
            throw new AuthenticationCredentialsNotFoundException("Error!");
        }
    }

    @Override
    public boolean supports(Class<?> authenticationType) {
    
    
        return UsernamePasswordAuthenticationToken.class.isAssignableFrom(authenticationType);
    }
}

正如你所看到的,这里if-else子句的条件取代了UserDetailsService和PasswordEncoder的责任。你不需要使用这两个Bean,但是如果你使用用户和密码进行认证,我强烈建议你把它们的管理逻辑分开。按照Spring Security架构的设计来应用它,即使你覆盖了认证实现。

你可能会发现通过实现你自己的AuthenticationProvider来替换认证逻辑是很有用的。如果默认的实现不能完全满足你的应用程序的要求,你可以决定实现自定义的认证逻辑。完整的AuthenticationProvider实现看起来像下一个列表中的那个。

在配置类中,你可以在configure(AuthenticationManagerBuilder auth)方法中注册AuthenticationProvider,如下表所示。

package com.laurentiuspilca.ssia.security;

import org.springframework.security.authentication.AuthenticationCredentialsNotFoundException;
import org.springframework.security.authentication.AuthenticationProvider;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.stereotype.Component;

import java.util.Arrays;

@Component
public class CustomAuthenticationProvider implements AuthenticationProvider {
    
    

    @Override
    public Authentication authenticate(Authentication authentication) throws AuthenticationException {
    
    
        //getName()方法是由Authentication从Principal接口继承的。
        String username = authentication.getName();
        String password = String.valueOf(authentication.getCredentials());

        //这个条件一般调用UserDetailsService和PasswordEncoder来测试用户名和密码。
        if ("john".equals(username) && "12345".equals(password)) {
    
    
            return new UsernamePasswordAuthenticationToken(username, password, Arrays.asList());
        } else {
    
    
            throw new AuthenticationCredentialsNotFoundException("Error!");
        }
    }

    @Override
    public boolean supports(Class<?> authenticationType) {
    
    
        return UsernamePasswordAuthenticationToken.class.isAssignableFrom(authenticationType);
    }
}

在配置类中,你可以在configure(AuthenticationManagerBuilder auth)方法中注册AuthenticationProvider,如下表所示。

清单2.16 注册AuthenticationProvider的新实现

package com.laurentiuspilca.ssia.config;

import com.laurentiuspilca.ssia.security.CustomAuthenticationProvider;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;

@Configuration
public class ProjectConfig extends WebSecurityConfigurerAdapter {
    
    

    @Autowired
    private CustomAuthenticationProvider authenticationProvider;

    @Override
    protected void configure(AuthenticationManagerBuilder auth) {
    
    
        auth.authenticationProvider(authenticationProvider);
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
    
    
        http.httpBasic();
        http.authorizeRequests()
                .anyRequest().authenticated();
    }
}

你现在可以调用端点,该端点可由唯一被认可的用户访问,如识别的唯一用户–John,密码为12345。

curl -u john:12345 http://localhost:8080/hello
Hello!

在第5章中,你将了解到关于AuthenticationProvider的更多细节,以及如何在认证过程中覆盖其行为。在那一章中,我们还将讨论Authentication接口及其实现,比如User-PasswordAuthenticationToken。

2.3.5 在你的项目中使用多个配置类

在之前实现的例子中,我们只使用了一个配置类。然而,即使对于配置类来说,分离责任也是很好的做法。我们需要这种分离,因为配置开始变得更加复杂。在一个生产就绪的应用程序中,你可能有比我们第一个例子中更多的声明。你可能也会发现有一个以上的配置类对项目的可读性很有帮助。

每个职责只有一个类,这始终是一个好的做法。在这个例子中,我们可以把用户管理配置和授权配置分开。我们通过定义两个配置类来做到这一点。UserManagementConfig(定义于清单2.17)和WebAuthorizationConfig(定义于清单2.18)。你可以在项目sia-ch2-ex6中找到这个例子。

清单2.17 定义用户和密码管理的配置类

package com.laurentiuspilca.ssia.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.password.NoOpPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.provisioning.InMemoryUserDetailsManager;

@Configuration
public class UserManagementConfig {
    
    

    @Bean
    public UserDetailsService userDetailsService() {
    
    
        var userDetailsService = new InMemoryUserDetailsManager();

        var user = User.withUsername("john")
                .password("12345")
                .authorities("read")
                .build();

        userDetailsService.createUser(user);
        return userDetailsService;
    }

    @Bean
    public PasswordEncoder passwordEncoder() {
    
    
        return NoOpPasswordEncoder.getInstance();
    }
}

在这种情况下,UserManagementConfig类只包含两个负责用户管理的Bean。UserDetailsService 和 PasswordEncoder。我们将把这两个对象配置成Bean,因为这个类不能扩展WebSecurityConfigurerAdapter。接下来的列表显示了这个定义。

清单2.18 定义授权管理的配置类

package com.laurentiuspilca.ssia.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 WebAuthorizationConfig extends WebSecurityConfigurerAdapter {
    
    

    @Override
    protected void configure(HttpSecurity http) throws Exception {
    
    
        http.httpBasic();
        http.authorizeRequests().anyRequest().authenticated();
    }
}

在这里,WebAuthorizationConfig类需要扩展WebSecurityConfigurerAdapter并重写configure(HttpSecurity http)方法。

注意 在这种情况下,你不能让两个类都扩展WebSecurityConfigurerAdapter。如果你这样做,依赖性注入就会失败。你可以通过使用@Order注解来设置注入的优先级来解决依赖性注入问题。但是,从功能上来说,这不会起作用,因为配置会互相排斥而不是合并。

总结

  • 当你将Spring Secu- rity添加到应用程序的依赖项中时,Spring Boot提供了一些默认配置。
  • 你可以实现认证和授权的基本组件。UserDetailsService、PasswordEncoder和AuthenticationProvider。
  • 你可以用用户类来定义用户。一个用户至少应该有一个用户名、一个密码和一个权限。权限是你允许用户在应用程序的上下文中进行的操作。
  • Spring Security提供的UserDetailsService的一个简单实现是InMemoryUserDetailsManager。你可以将用户添加到这样一个UserDetailsService的实例中,以便在应用程序的内存中管理用户。
  • NoOpPasswordEncoder是PasswordEncoder合约的一个实现,它使用明文的密码。这个实现适合于学习的例子和(也许)概念的证明,但不适合生产准备的应用。
  • 你可以使用AuthenticationProvider合约来实现应用程序中的自定义认证逻辑。
  • 有多种方法来编写配置,但在单个应用程序中,你应该选择并坚持使用一种方法。这有助于使你的代码更简洁,更容易理解。

猜你喜欢

转载自blog.csdn.net/Learning_xzj/article/details/128998466