0%

Spring-security

先决条件

Spring Security需要Java 8或更高版本的运行时环境。

由于Spring Security旨在以独立的方式运行,因此您无需在Java Runtime Environment中放置任何特殊的配置文件。 特别是,您无需配置特殊的Java身份验证和授权服务(JAAS)策略文件,也无需将Spring Security放置在公共类路径位置。

同样,如果使用EJB容器或Servlet容器,则无需在任何地方放置任何特殊的配置文件,也无需在服务器类加载器中包含Spring Security。 所有必需的文件都包含在您的应用程序中。

这种设计提供了最大的部署时间灵活性,因为您可以将目标工件(JAR,WAR或EAR)从一个系统复制到另一个系统,并且可以立即使用。

Spring安全社区

欢迎来到Spring Security社区! 本节讨论如何充分利用我们庞大的社区。

获得帮助

如果您需要有关Spring Security的帮助,我们将在这里为您提供帮助。 以下是获得帮助的一些最佳方法:

  • 通读本文档。

参与其中

我们欢迎您参与Spring Security项目。 有很多贡献方式,包括回答有关StackOverflow的问题,编写新代码,改进现有代码,协助编写文档,开发示例或教程,报告错误或仅提出建议。 有关更多信息,请参见我们的贡献文档

源代码

您可以在GitHub上找到Spring Security的源代码,网址为https://github.com/spring-projects/spring-security/

Apache 2许可证

Spring Security是根据Apache 2.0许可发布的开源软件。

社交媒体

您可以在Twitter上关注@SpringSecurity和Spring Security团队,以获取最新消息。 您还可以关注@SpringCentral,以了解整个Spring产品组合的最新信息。

Spring Security 5.3的新增功能

Spring Security 5.3提供了许多新功能。 以下是该版本的重点内容。

文档更新

我们将继续努力重写文档。

在此版本中,您将看到以下内容:

添加了Kotlin样本

Reskinned

  • 添加了滚动菜单

  • 添加了切换

  • 更新样式

获得Spring Security

本节讨论了有关获取Spring Security二进制文件所需的所有知识。 有关如何获取源代码的信息,请参见源代码

发布编号

Spring Security版本的格式为MAJOR.MINOR.PATCH,使得:

  • 主要版本可能包含重大更改。通常,这样做是为了提供改进的安全性以匹配现代安全性实践。

  • MINOR版本包含增强功能,但被视为被动更新

  • PATCH级别应该是完全兼容的,向前和向后兼容,但可能存在修正错误的更改除外。

与Maven结合使用

与大多数开源项目一样,Spring Security将其依赖项部署为Maven工件。本节中的主题提供有关使用Maven时如何使用Spring Security的详细信息。

Spring Boot with Maven

Spring Boot提供了一个spring-boot-starter-security入门程序,它将Spring Security相关的依赖项聚合在一起。使用启动程序的最简单且首选的方法是通过IDE集成(Eclipse,IntelliJ,NetBeans)或通过使用Spring Initializr)。

另外,您可以手动添加启动器,如以下示例所示:

1
2
3
4
5
6
7
<dependencies>
<!-- ... other dependency elements ... -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
</dependencies>

由于Spring Boot提供了Maven BOM来管理依赖版本,因此您无需指定版本。 如果您希望覆盖Spring Security版本,可以通过提供Maven属性来实现,如以下示例所示:

1
2
3
4
<properties>
<!-- ... -->
<spring-security.version>5.3.4.RELEASE</spring-security.version>
</dependencies>

由于Spring Security仅在主要版本中进行重大更改,因此可以将较新版本的Spring Security与Spring Boot一起使用是安全的。 但是,有时您可能还需要更新Spring Framework的版本。 您可以通过添加Maven属性来执行此操作,如以下示例所示:

1
2
3
4
<properties>
<!-- ... -->
<spring.version>5.2.8.RELEASE</spring.version>
</dependencies>

如果使用其他功能(例如LDAP,OpenID和其他功能),则还需要包括适当的项目模块

没有Spring Boot的Maven

使用不带Spring Boot的Spring Security时,首选方法是使用Spring Security的BOM,以确保在整个项目中使用一致的Spring Security版本。 以下示例显示了如何执行此操作:

1
2
3
4
5
6
7
8
9
10
11
12
<dependencyManagement>
<dependencies>
<!-- ... other dependency elements ... -->
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-bom</artifactId>
<version>{spring-security-version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>

最小的Spring Security Maven依赖关系集通常如下所示:

1
2
3
4
5
6
7
8
9
10
11
<dependencies>
<!-- ... other dependency elements ... -->
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-config</artifactId>
</dependency>
</dependencies>

如果使用其他功能(例如LDAP,OpenID和其他功能),则还需要包括适当的项目模块

Spring Security是基于Spring Framework 5.2.8.RELEASE构建的,但通常应与任何较新版本的Spring Framework 5.x一起使用。 Spring Security的可传递依赖项解决了Spring Framework 5.2.8.RELEASE的事实,这可能会引起奇怪的类路径问题,因此许多用户可能会误以为是。 解决此问题的最简单方法是在pom.xml<dependencyManagement>部分中使用spring-framework-bom,如以下示例所示:

1
2
3
4
5
6
7
8
9
10
11
12
<dependencyManagement>
<dependencies>
<!-- ... other dependency elements ... -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-framework-bom</artifactId>
<version>5.2.8.RELEASE</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>

前面的示例确保Spring Security的所有传递依赖项都使用Spring 5.2.8.RELEASE模块。

Note

这种方法使用Maven的“物料清单”(BOM)概念,并且仅在Maven 2.0.9+中可用。 有关如何解决依赖关系的其他详细信息,请参阅Maven的“依赖关系机制简介”文档。

Maven仓库

所有GA版本(即以.RELEASE结尾的版本)均已部署到Maven Central,因此无需在pom中声明其他Maven存储库。

如果使用SNAPSHOT版本,则需要确保定义了Spring Snapshot存储库,如以下示例所示:

1
2
3
4
5
6
7
8
<repositories>
<!-- ... possibly other repository elements ... -->
<repository>
<id>spring-snapshot</id>
<name>Spring Snapshot Repository</name>
<url>https://repo.spring.io/snapshot</url>
</repository>
</repositories>

如果使用里程碑版本或候选版本,则需要确保定义了Spring Milestone存储库,如以下示例所示:

1
2
3
4
5
6
7
8
<repositories>
<!-- ... possibly other repository elements ... -->
<repository>
<id>spring-milestone</id>
<name>Spring Milestone Repository</name>
<url>https://repo.spring.io/milestone</url>
</repository>
</repositories>

Gradle

与大多数开源项目一样,Spring Security将其依赖项部署为Maven工件,从而提供了一流的Gradle支持。 以下主题详细介绍了使用Gradle时如何使用Spring Security。

Spring Boot with Gradle

Spring Boot提供了一个spring-boot-starter-security入门工具,它将Spring Security相关的依赖项聚合在一起。 使用启动程序的最简单且首选的方法是通过IDE集成(Eclipse,IntelliJ,NetBeans)或通过https://start.spring.io使用Spring Initializr。

另外,您可以手动添加启动器,如以下示例所示:

1
2
3
dependencies {
compile "org.springframework.boot:spring-boot-starter-security"
}

由于Spring Boot提供了Maven BOM来管理依赖版本,因此您无需指定版本。 如果您想覆盖Spring Security版本,可以通过提供Gradle属性来实现,如以下示例所示:

1
ext['spring-security.version']='5.3.4.RELEASE'

由于Spring Security仅在主要版本中进行重大更改,因此可以将较新版本的Spring Security与Spring Boot一起使用是安全的。 但是,有时您可能还需要更新Spring Framework的版本。 您可以通过添加Gradle属性来执行此操作,如以下示例所示:

1
ext['spring.version']='5.2.8.RELEASE'

如果使用其他功能(例如LDAP,OpenID和其他功能),则还需要包括适当的项目模块。

Gradle Without Spring Boot

使用不带Spring Boot的Spring Security时,首选方法是使用Spring Security的BOM,以确保在整个项目中使用一致的Spring Security版本。 您可以使用Dependency Management插件来做到这一点,如以下示例所示:

1
2
3
4
5
6
7
8
9
plugins {
id "io.spring.dependency-management" version "1.0.6.RELEASE"
}

dependencyManagement {
imports {
mavenBom 'org.springframework.security:spring-security-bom:5.3.4.RELEASE'
}
}

最小的Spring Security Maven依赖关系集通常如下所示:

1
2
3
4
dependencies {
compile "org.springframework.security:spring-security-web"
compile "org.springframework.security:spring-security-config"
}

如果使用其他功能(例如LDAP,OpenID和其他功能),则还需要包括适当的项目模块

Spring Security是基于Spring Framework 5.2.8.RELEASE构建的,但通常应与任何较新版本的Spring Framework 5.x一起使用。 Spring Security的可传递依赖项解决了Spring Framework 5.2.8.RELEASE的事实,这可能会引起奇怪的类路径问题,因此许多用户可能会误以为是。 解决此问题的最简单方法是在pom.xml<dependencyManagement>部分中使用spring-framework-bom。 您可以使用Dependency Management插件来做到这一点,如以下示例所示:

1
2
3
4
5
6
7
8
9
plugins {
id "io.spring.dependency-management" version "1.0.6.RELEASE"
}

dependencyManagement {
imports {
mavenBom 'org.springframework:spring-framework-bom:5.2.8.RELEASE'
}
}

前面的示例确保Spring Security的所有传递依赖项都使用Spring 5.2.8.RELEASE模块。

Gradle仓库

所有GA版本(即以.RELEASE结尾的版本)都已部署到Maven Central,因此使用mavenCentral()存储库足以满足GA版本的要求。 以下示例显示了如何执行此操作:

1
2
3
repositories {
mavenCentral()
}

如果使用SNAPSHOT版本,则需要确保定义了Spring Snapshot存储库,如以下示例所示:

1
2
3
repositories {
maven { url 'https://repo.spring.io/snapshot' }
}

如果使用里程碑版本或候选版本,则需要确保定义了Spring Milestone存储库,如以下示例所示:

1
2
3
repositories {
maven { url 'https://repo.spring.io/milestone' }
}

特征

Spring Security为身份验证,授权和针对常见漏洞的防护提供了全面的支持。 它还提供与其他库的集成,以简化其使用。

认证方式

Spring Security为身份验证提供了全面的支持。 身份验证是我们验证谁试图访问特定资源的身份的方法。 验证用户身份的常用方法是要求用户输入用户名和密码。 一旦执行了身份验证,我们就会知道身份并可以执行授权。

认证支持

Spring Security提供了用于验证用户的内置支持。有关每个堆栈支持的内容的详细信息,请参阅Servlet和WebFlux的身份验证部分。

密码储存

Spring Security的PasswordEncoder接口用于对密码进行单向转换,以使密码可以安全地存储。鉴于PasswordEncoder是一种单向转换,因此当密码转换需要采用两种方式(即存储用于向数据库进行身份验证的凭据)时,则不打算使用它。通常,PasswordEncoder用于存储在身份验证时需要与用户提供的密码进行比较的密码。

密码存储历史

多年来,用于存储密码的标准机制已经发展。最初,密码以纯文本格式存储。假定密码是安全的,因为数据存储密码已保存在访问它所需的凭据中。但是,恶意用户能够使用SQL Injection这样的攻击找到方法来获取用户名和密码的大型“数据转储”。随着越来越多的用户凭证成为公共安全专家,我们意识到我们需要做更多的工作来保护用户的密码。

然后鼓励开发人员在通过诸如SHA-256之类的单向哈希运行密码后存储密码。当用户尝试进行身份验证时,会将哈希密码与他们键入的密码的哈希进行比较。这意味着系统仅需要存储密码的单向哈希。如果发生违规,则仅暴露密码的一种单列哈希。由于散列是一种方式,计算出给定哈希值的密码很难计算,因此找出系统中的每个密码都不值得。为了击败这个新系统,恶意用户决定创建称为Rainbow Tables的查找表。他们无需每次都猜测每个密码,而是只计算一次密码并将其存储在查找表中。

为了减轻Rainbow Tables的有效性,鼓励开发人员使用加盐的密码。不仅将密码用作哈希函数的输入,还将为每个用户的密码生成随机字节(称为salt)。盐和用户密码将通过散列函数运行,从而产生唯一的散列。盐将以明文形式与用户密码一起存储。然后,当用户尝试进行身份验证时,会将哈希密码与存储的盐的哈希值和他们键入的密码进行比较。唯一的盐意味着Rainbow Tables不再有效,因为每种盐和密码组合的哈希值都不同。

在现代,我们意识到密码哈希(例如SHA-256)不再安全。原因是使用现代硬件,我们可以每秒执行数十亿次哈希计算。这意味着我们可以轻松地分别破解每个密码。

现在鼓励开发人员利用自适应单向功能来存储密码。具有自适应单向功能的密码验证有意占用大量资源(即CPU,内存等)。自适应单向功能允许配置“工作因数”,该因数会随着硬件的改进而增加。建议将“工作因数”调整为大约1秒钟,以验证系统上的密码。这种权衡使攻击者难以破解密码,但代价却不那么高,这给您自己的系统增加了负担。 Spring Security试图为“工作因素”提供一个良好的起点,但是鼓励用户为自己的系统自定义“工作因素”,因为不同系统之间的性能会有很大的不同。应使用的自适应单向功能示例包括bcryptPBKDF2scryptargon2

由于自适应单向功能有意占用大量资源,因此为每个请求验证用户名和密码都会大大降低应用程序的性能。 Spring Security(或任何其他库)无法执行任何操作来加快密码的验证速度,因为通过增加验证资源的强度来获得安全性。鼓励用户将长期凭证(即用户名和密码)交换为短期凭证(即会话,OAuth令牌等)。可以快速验证短期凭证,而不会损失任何安全性。

DelegatingPasswordEncoder

在Spring Security 5.0之前,默认的PasswordEncoderNoOpPasswordEncoder,它需要纯文本密码。 根据“密码历史记录”部分,您可能希望默认的PasswordEncoder现在类似于BCryptPasswordEncoder。 但是,这忽略了三个现实问题:

  • 有许多使用旧密码编码的应用程序无法轻松迁移
  • 密码存储的最佳做法将再次更改。
  • 作为一个框架,Spring Security不能经常进行重大更改

相反,Spring Security引入了DelegatingPasswordEncoder,它通过以下方式解决了所有问题:

  • 确保使用当前密码存储建议对密码进行编码
  • 允许以现代和旧式格式验证密码
  • 允许将来升级编码

您可以使用PasswordEncoderFactories轻松构造DelegatingPasswordEncoder的实例。

例子18.创建默认的DelegatingPasswordEncoder

1
2
PasswordEncoder passwordEncoder =
PasswordEncoderFactories.createDelegatingPasswordEncoder();
1
val passwordEncoder: PasswordEncoder = PasswordEncoderFactories.createDelegatingPasswordEncoder()

或者,您可以创建自己的自定义实例。 例如:

例子19.创建自定义DelegatingPasswordEncoder

1
2
3
4
5
6
7
8
9
10
String idForEncode = "bcrypt";
Map encoders = new HashMap<>();
encoders.put(idForEncode, new BCryptPasswordEncoder());
encoders.put("noop", NoOpPasswordEncoder.getInstance());
encoders.put("pbkdf2", new Pbkdf2PasswordEncoder());
encoders.put("scrypt", new SCryptPasswordEncoder());
encoders.put("sha256", new StandardPasswordEncoder());

PasswordEncoder passwordEncoder =
new DelegatingPasswordEncoder(idForEncode, encoders);
1
2
3
4
5
6
7
8
9
val idForEncode = "bcrypt"
val encoders: MutableMap<String, PasswordEncoder> = mutableMapOf()
encoders[idForEncode] = BCryptPasswordEncoder()
encoders["noop"] = NoOpPasswordEncoder.getInstance()
encoders["pbkdf2"] = Pbkdf2PasswordEncoder()
encoders["scrypt"] = SCryptPasswordEncoder()
encoders["sha256"] = StandardPasswordEncoder()

val passwordEncoder: PasswordEncoder = DelegatingPasswordEncoder(idForEncode, encoders)

密码存储格式

密码的一般格式为:

例子20. DelegatingPasswordEncoder存储格式

1
{id}encodedPassword

这样的id是用于查找应使用哪个PasswordEncoder的标识符,而encodePassword是所选PasswordEncoder的原始编码密码。 ID必须在密码的开头,以{开头,以}结尾。 如果找不到该ID,则该ID将为null。 例如,以下可能是使用不同ID编码的密码列表。 所有原始密码均为“密码”。

例子21. DelegatingPasswordEncoder编码密码例子

1
2
3
4
5
{bcrypt}$2a$10$dXJ3SW6G7P50lGmMkkmwe.20cQQubK3.HZWzG3YB1tlRy.fqvM/BG 1
{noop}password 2
{pbkdf2}5d923b44a6d129f3ddf3e3c8d29412723dcbde72445e8ef6bf3b508fbf17fa4ed4d6b99ca763d8dc 3
{scrypt}$e0801$8bWJaSu2IKSn9Z9kM+TPXfOc/9bdYSrN1oD9qfVThWEwdRTnO7re7Ei+fUZRJ68k9lTyuTeUp4of4g24hHnazw==$OAOec05+bXxvuu/1qZ6NUR+xQYvYv7BeL1QxwRpY5Pc= 4
{sha256}97cde38028ad898ebc02e690819fa220e88c62e0699403e94fff291cfffaf8410849f27605abcbc0 5
  1. 第一个密码将具有bcrypt的PasswordEncoder id和编码密码为$2a$10$dXJ3SW6G7P50lGmMkkmwe.20cQQubK3.HZWzG3YB1tlRy.fqvM/BG
    匹配时将委派给BCryptPasswordEncoder
  2. 第二个密码将具有noop的PasswordEncoder id和密码的encodePassword。
    匹配时将委派给NoOpPasswordEncoder
  3. 第三个密码的密码编码器ID为pbkdf2,编码密码为5d923b44a6d129f3ddf3e3c8d29412723dcbde72445e8ef6bf3b508fbf17fa4ed4d6b99ca763d8dc。
    匹配时将委派给Pbkdf2PasswordEncoder
  4. 第四个密码将具有scrypt的密码编码器ID,并且编码的密码为$e0801$8bWJaSu2IKSn9Z9kM+TPXfOc/9bdYSrN1oD9qfVThWEwdRTnO7re7Ei+fUZRJ68k9lTyuTeUp4of4g24hHnazw==$OAOec05+bXxvuu/1qZ6NUR+xQYvYv7BeL1QxwRpY5Pc=
    匹配时将委派给SCryptPasswordEncoder
  5. 最终密码的密码编码器ID为sha256,编码密码为97cde38028ad898ebc02e690819fa220e88c62e0699403e94fff291cfffaf8410849f27605abcbc0。
    匹配时将委派给StandardPasswordEncoder

Note

一些用户可能会担心为潜在的黑客提供了存储格式。不必担心,因为密码的存储不依赖于算法是秘密。此外,大多数格式很容易让攻击者在没有前缀的情况下弄清楚。例如,BCrypt密码通常以$ 2a $开头。

密码编码

传递给构造函数的idForEncode确定将使用哪个PasswordEncoder编码密码。 在上面我们构造的DelegatingPasswordEncoder中,这意味着编码密码的结果将委派给BCryptPasswordEncoder并以{bcrypt}为前缀。 最终结果如下所示:

例子22. DelegatingPasswordEncoder编码例子

1
{bcrypt}$2a$10$dXJ3SW6G7P50lGmMkkmwe.20cQQubK3.HZWzG3YB1tlRy.fqvM/BG
密码匹配

匹配是基于{id}id到构造函数中提供的PasswordEncoder的映射完成的。我们的“密码存储格式”示例提供了如何完成此操作的示例。默认情况下,使用密码和未映射的ID(包括空ID)调用match(CharSequence,String)的结果将导致IllegalArgumentException。可以使用DelegatingPasswordEncoder.setDefaultPasswordEncoderForMatches(PasswordEncoder)自定义此行为。

通过使用id,我们可以匹配任何密码编码,但是使用最现代的密码编码对密码进行编码。这很重要,因为与加密不同,密码哈希被设计为没有简单的方法来恢复明文。由于无法恢复明文,因此很难迁移密码。虽然用户迁移NoOpPasswordEncoder很容易,但我们默认选择包含它,以使入门体验更简单。

入门经验

如果您要编写演示或样本,那么花一些时间来哈希用户密码会很麻烦。有一些方便的机制可以简化此过程,但这仍然不适合生产。

例子23. withDefaultPasswordEncoder例子

1
2
3
4
5
6
7
User user = User.withDefaultPasswordEncoder()
.username("user")
.password("password")
.roles("user")
.build();
System.out.println(user.getPassword());
// {bcrypt}$2a$10$dXJ3SW6G7P50lGmMkkmwe.20cQQubK3.HZWzG3YB1tlRy.fqvM/BG
1
2
3
4
5
6
7
val user = User.withDefaultPasswordEncoder()
.username("user")
.password("password")
.roles("user")
.build()
println(user.password)
// {bcrypt}$2a$10$dXJ3SW6G7P50lGmMkkmwe.20cQQubK3.HZWzG3YB1tlRy.fqvM/BG

如果要创建多个用户,则还可以重复使用该构建器。

例子24. withDefaultPasswordEncoder重用构建器

1
2
3
4
5
6
7
8
9
10
11
UserBuilder users = User.withDefaultPasswordEncoder();
User user = users
.username("user")
.password("password")
.roles("USER")
.build();
User admin = users
.username("admin")
.password("password")
.roles("USER","ADMIN")
.build();
1
2
3
4
5
6
7
8
9
10
11
val users = User.withDefaultPasswordEncoder()
val user = users
.username("user")
.password("password")
.roles("USER")
.build()
val admin = users
.username("admin")
.password("password")
.roles("USER", "ADMIN")
.build()

这会散列存储的密码,但是密码仍在内存和已编译的源代码中公开。 因此,对于生产环境,仍不认为它是安全的。 为了进行生产,您应该在外部对密码进行哈希处理

使用Spring Boot CLI编码

正确编码密码的最简单方法是使用Spring Boot CLI

例如,以下代码将对与DelegatingPasswordEncoder一起使用的password密码进行编码:

例子25. Spring Boot CLI encodepassword例子

1
2
spring encodepassword password
{bcrypt}$2a$10$X5wFBtLrL/kHcmrOGGTrGufsBX8CJ0WpQpF3pgeuxBB/H73BK1DW6
故障排除

当所存储的密码之一没有如“密码存储格式”中所述的id时,会发生以下错误。

1
2
3
java.lang.IllegalArgumentException: There is no PasswordEncoder mapped for the id "null"
at org.springframework.security.crypto.password.DelegatingPasswordEncoder$UnmappedIdPasswordEncoder.matches(DelegatingPasswordEncoder.java:233)
at org.springframework.security.crypto.password.DelegatingPasswordEncoder.matches(DelegatingPasswordEncoder.java:196)

解决该错误的最简单方法是切换为显式提供用于编码密码的PasswordEncoder。 解决此问题的最简单方法是弄清楚密码的当前存储方式,并明确提供正确的PasswordEncoder

如果您是从Spring Security 4.2.x迁移的,则可以通过公开NoOpPasswordEncoder bean恢复到以前的行为。

或者,您可以为所有密码加上正确的ID前缀,然后继续使用DelegatingPasswordEncoder。 例如,如果您使用的是BCrypt,则可以从以下方式迁移密码:

1
$2a$10$dXJ3SW6G7P50lGmMkkmwe.20cQQubK3.HZWzG3YB1tlRy.fqvM/BG

1
{bcrypt}$2a$10$dXJ3SW6G7P50lGmMkkmwe.20cQQubK3.HZWzG3YB1tlRy.fqvM/BG

有关映射的完整列表,请参阅PasswordEncoderFactories上的Javadoc。

BCryptPasswordEncoder

BCryptPasswordEncoder实现使用广泛支持的bcrypt算法对密码进行哈希处理。 为了使其更能抵御密码破解,bcrypt故意降低了速度。 与其他自适应单向功能一样,应将其调整为大约1秒钟才能验证系统上的密码。 BCryptPasswordEncoder的默认实现使用强度10,如BCryptPasswordEncoder的Javadoc中所述。 建议您在自己的系统上调整和测试强度参数,以使验证密码大约需要1秒钟。

例子26. BCryptPasswordEncoder

1
2
3
4
// Create an encoder with strength 16
BCryptPasswordEncoder encoder = new BCryptPasswordEncoder(16);
String result = encoder.encode("myPassword");
assertTrue(encoder.matches("myPassword", result));
1
2
3
4
// Create an encoder with strength 16
val encoder = BCryptPasswordEncoder(16)
val result: String = encoder.encode("myPassword")
assertTrue(encoder.matches("myPassword", result))

Argon2PasswordEncoder

Argon2PasswordEncoder实现使用Argon2算法对密码进行哈希处理。 Argon2是“密码哈希竞赛”的获胜者。 为了克服自定义硬件上的密码破解问题,Argon2是一种故意慢速的算法,需要大量内存。 与其他自适应单向功能一样,应将其调整为大约1秒钟才能验证系统上的密码。 Argon2PasswordEncoder的当前实现需要BouncyCastle。

例子27. Argon2PasswordEncoder

1
2
3
4
/ Create an encoder with all the defaults
Argon2PasswordEncoder encoder = new Argon2PasswordEncoder();
String result = encoder.encode("myPassword");
assertTrue(encoder.matches("myPassword", result));
1
2
3
4
// Create an encoder with all the defaults
val encoder = Argon2PasswordEncoder()
val result: String = encoder.encode("myPassword")
assertTrue(encoder.matches("myPassword", result))

Pbkdf2PasswordEncoder

Pbkdf2PasswordEncoder实现使用PBKDF2算法对密码进行哈希处理。 为了克服密码破解问题,PBKDF2是一种故意缓慢的算法。 与其他自适应单向功能一样,应将其调整为大约1秒钟才能验证系统上的密码。 当需要FIPS认证时,此算法是不错的选择。

例子28. Pbkdf2PasswordEncoder

1
2
3
4
// Create an encoder with all the defaults
Pbkdf2PasswordEncoder encoder = new Pbkdf2PasswordEncoder();
String result = encoder.encode("myPassword");
assertTrue(encoder.matches("myPassword", result));
1
2
3
4
// Create an encoder with all the defaults
val encoder = Pbkdf2PasswordEncoder()
val result: String = encoder.encode("myPassword")
assertTrue(encoder.matches("myPassword", result))

SCryptPasswordEncoder

SCryptPasswordEncoder实现使用scrypt算法对密码进行哈希处理。 为了克服自定义硬件scrypt上的密码破解问题,这是一种故意缓慢的算法,需要大量内存。 与其他自适应单向功能一样,应将其调整为大约1秒钟才能验证系统上的密码。

例子29. SCryptPasswordEncoder

1
2
3
4
// Create an encoder with all the defaults
SCryptPasswordEncoder encoder = new SCryptPasswordEncoder();
String result = encoder.encode("myPassword");
assertTrue(encoder.matches("myPassword", result));
1
2
3
4
// Create an encoder with all the defaults
val encoder = SCryptPasswordEncoder()
val result: String = encoder.encode("myPassword")
assertTrue(encoder.matches("myPassword", result))

其他密码编码器

还有许多其他的PasswordEncoder实现完全是为了向后兼容而存在的。 不推荐使用它们,以表明它们不再被视为安全。 但是,由于很难迁移现有的旧系统,因此没有删除它们的计划。

密码存储配置

Spring Security默认使用DelegatingPasswordEncoder。 但是,可以通过将PasswordEncoder公开为Spring bean来对其进行自定义。

如果您是从Spring Security 4.2.x迁移的,则可以通过公开NoOpPasswordEncoder bean恢复到以前的行为。

Warning

恢复为NoOpPasswordEncoder不被认为是安全的。 相反,您应该迁移到使用DelegatingPasswordEncoder支持安全密码编码。

例子30. NoOpPasswordEncoder

1
2
3
4
@Bean
public static NoOpPasswordEncoder passwordEncoder() {
return NoOpPasswordEncoder.getInstance();
}
1
2
<b:bean id="passwordEncoder"
class="org.springframework.security.crypto.password.NoOpPasswordEncoder" factory-method="getInstance"/>
1
2
3
4
@Bean
fun passwordEncoder(): PasswordEncoder {
return NoOpPasswordEncoder.getInstance();
}

Note

XML配置要求NoOpPasswordEncoder Bean名称为passwordEncoder

防止利用漏洞

Spring Security提供了针对常见漏洞的保护。只要有可能,默认情况下就启用保护。在下面,您将找到Spring Security防御的各种利用的高级描述。

跨站请求伪造(CSRF)

Spring为防止跨站请求伪造(CSRF)攻击提供了全面的支持。在以下各节中,我们将探讨:

Note

本文档的这一部分讨论CSRF保护的一般主题。有关针对Servlet和基于WebFlux的应用程序的CSRF保护的特定信息,请参阅相关部分。

什么是CSRF攻击?

理解CSRF攻击的最佳方法是看一个具体示例。

假设您银行的网站提供了一种表单,该表格允许将资金从当前登录的用户转移到另一个银行帐户。例如,转移表单可能如下所示:

例子31.转移表单

1
2
3
4
5
6
<form method="post" action="/transfer">
<input type="text" name="amount"/>
<input type="text" name="routingNumber"/>
<input type="text" name="account"/>
<input type="submit" value="Transfer"/>
</form>

相应的HTTP请求可能如下所示:

例子32.传输HTTP请求

1
2
3
4
5
6
POST /transfer HTTP/1.1
Host: bank.example.com
Cookie: JSESSIONID=randomid
Content-Type: application/x-www-form-urlencoded

amount=100.00&routingNumber=1234&account=9876

现在,假装您对银行的网站进行身份验证,然后无需注销即可访问一个邪恶的网站。 恶意网站包含具有以下格式的HTML页面:

例子33.邪恶的转移表单

1
2
3
4
5
6
<form method="post" action="https://bank.example.com/transfer">
<input type="hidden" name="amount" value="100.00"/>
<input type="hidden" name="routingNumber" value="evilsRoutingNumber"/>
<input type="hidden" name="account" value="evilsAccountNumber"/>
<input type="submit" value="Win Money!"/>
</form>

您想赢钱,因此单击“提交”按钮。在此过程中,您无意中将$ 100转让给了恶意用户。发生这种情况的原因是,尽管恶意网站无法看到您的cookie,但与您的银行关联的cookie仍与请求一起发送。

最糟糕的是,使用JavaScript可以使整个过程自动化。这意味着您甚至不需要单击该按钮。此外,在访问受XSS攻击)的诚实站点时,也很容易发生这种情况。那么,我们如何保护用户免受此类攻击呢?

防范CSRF攻击

发生CSRF攻击的原因是受害者网站的HTTP请求与攻击者网站的请求完全相同。这意味着无法拒绝来自邪恶网站的请求,也不允许来自银行网站的请求。为了防御CSRF攻击,我们需要确保恶意站点无法提供请求中的某些内容,因此我们可以区分这两个请求。

Spring提供了两种机制来防御CSRF攻击:

Note

两种保护都要求安全方法必须是幂等的

安全方法必须是幂等的

为了使针对CSRF的任何一种保护都起作用,应用程序必须确保“安全” HTTP方法是幂等的。这意味着使用HTTP方法GETHEADOPTIONSTRACE进行的请求不应更改应用程序的状态。

同步器令牌模式

抵御CSRF攻击的最主要,最全面的方法是使用同步器令牌模式。该解决方案是为了确保每个HTTP请求除了我们的会话cookie之外,还必须在HTTP请求中包含一个安全的,随机生成的值,称为CSRF令牌。

提交HTTP请求时,服务器必须查找预期的CSRF令牌,并将其与HTTP请求中的实际CSRF令牌进行比较。如果值不匹配,则应拒绝HTTP请求。

这项工作的关键是,实际的CSRF令牌应该位于浏览器不会自动包含的HTTP请求的一部分中。例如,在HTTP参数或HTTP标头中要求实际的CSRF令牌将防止CSRF攻击。在cookie中要求实际CSRF令牌不起作用,因为浏览器会自动将cookie包含在HTTP请求中。

我们可以放宽期望,仅对每个更新应用程序状态的HTTP请求仅要求实际的CSRF令牌。为此,我们的应用程序必须确保安全的HTTP方法是幂等的。因为我们希望允许使用外部站点的链接来链接到我们的网站,所以这提高了可用性。另外,我们不想在HTTP GET中包含随机令牌,因为这可能导致令牌泄漏。

让我们看一下使用同步令牌模式时示例将如何变化。假设实际的CSRF令牌必须位于名为_csrf的HTTP参数中。我们应用程序的转帐表单如下:

例子34.同步器令牌形式

1
2
3
4
5
6
7
<form method="post" action="/transfer">
<input type="hidden" name="_csrf" value="4bfd1575-3ad1-4d21-96c7-4ef2d9f86721"/>
<input type="text" name="amount"/>
<input type="text" name="routingNumber"/>
<input type="hidden" name="account"/>
<input type="submit" value="Transfer"/>
</form>

现在,该表单包含具有CSRF令牌值的隐藏输入。 外部站点无法读取CSRF令牌,因为相同的来源策略可确保恶意站点无法读取响应。

相应的HTTP汇款请求如下所示:

例子35.同步器令牌请求

1
2
3
4
5
6
POST /transfer HTTP/1.1
Host: bank.example.com
Cookie: JSESSIONID=randomid
Content-Type: application/x-www-form-urlencoded

amount=100.00&routingNumber=1234&account=9876&_csrf=4bfd1575-3ad1-4d21-96c7-4ef2d9f86721

您会注意到,HTTP请求现在包含带有安全随机值的_csrf参数。恶意网站将无法为_csrf参数提供正确的值(必须在邪恶网站上明确提供),并且当服务器将实际CSRF令牌与预期CSRF令牌进行比较时,传输将失败。

SameSite属性

防止CSRF攻击的一种新兴方法是在cookie上指定SameSite属性。设置cookie时,服务器可以指定SameSite属性,以指示从外部站点发出时不应发送该cookie。

Note

Spring Security不直接控制会话cookie的创建,因此不提供对SameSite属性的支持。 Spring Session在基于servlet的应用程序中为SameSite属性提供支持。 Spring Framework的CookieWebSessionIdResolver为基于WebFlux的应用程序中的SameSite属性提供了开箱即用的支持。

一个示例,带有SameSite属性的HTTP响应标头可能类似于:

例子36. SameSite HTTP响应

1
Set-Cookie: JSESSIONID=randomid; Domain=bank.example.com; Secure; HttpOnly; SameSite=Lax

SameSite属性的有效值为:

  • Strict-指定后,来自同一站点的任何请求都将包含cookie。否则,cookie将不会包含在HTTP请求中。

  • Lax-当来自同一个站点的请求发送的cookie或来自顶层导航的请求且方法是幂等的时候。否则,cookie将不会包含在HTTP请求中。

让我们看一下如何使用SameSite属性保护我们的示例。银行应用程序可以通过在会话cookie上指定SameSite属性来防御CSRF。

在会话cookie上设置SameSite属性后,浏览器将继续发送JSESSIONID cookie和来自银行网站的请求。但是,浏览器将不再发送带有来自邪恶网站的传输请求的JSESSIONID cookie。由于会话不再存在于来自邪恶网站的传输请求中,因此可以保护应用程序免受CSRF攻击。

使用SameSite属性防御CSRF攻击时,应注意一些重要注意事项

SameSite属性设置为Strict可以提供更强的防御能力,但会使用户感到困惑。考虑一个保持登录到托管在https://social.example.com上的社交媒体网站的用户。用户在https://email.example.org上收到一封电子邮件,其中包含指向社交媒体网站的链接。如果用户单击链接,则他们理所当然地希望能够通过社交媒体站点进行身份验证。但是,如果`SameSite`属性为`Strict`,则不会发送cookie,因此不会对用户进行身份验证。

Note

通过实施gh-7537,我们可以提高SameSite防御CSRF攻击的保护性和可用性。

另一个明显的考虑因素是,为了使SameSite属性能够保护用户,浏览器必须支持SameSite属性。大多数现代浏览器都支持SameSite属性。但是,可能仍未使用较旧的浏览器。

因此,通常建议将SameSite属性用作深度防御,而不是针对CSRF攻击的唯一防护。

何时使用CSRF保护

什么时候应该使用CSRF保护?我们的建议是对普通用户可能由浏览器处理的任何请求使用CSRF保护。如果仅创建非浏览器客户端使用的服务,则可能需要禁用CSRF保护。

CSRF保护和JSON

一个常见的问题是“我需要保护由javascript发出的JSON请求吗?”简短的答案是,这视情况而定。但是,您必须非常小心,因为有些CSRF漏洞会影响JSON请求。例如,恶意用户可以使用以下格式使用JSON创建CSRF

例子37. CSRF和JSON形式

1
2
3
4
<form action="https://bank.example.com/transfer" method="post" enctype="text/plain">
<input name='{"amount":100,"routingNumber":"evilsRoutingNumber","account":"evilsAccountNumber", "ignore_me":"' value='test"}' type='hidden'>
<input type="submit" value="Win Money!"/>
</form>

这将产生以下JSON结构

例子38. CSRF和JSON请求

1
2
3
4
5
{ "amount": 100,
"routingNumber": "evilsRoutingNumber",
"account": "evilsAccountNumber",
"ignore_me": "=test"
}

如果应用程序未验证Content-Type,则该应用程序将被暴露。 根据设置的不同,仍然可以通过更新URL后缀以.json结尾来利用验证内容类型的Spring MVC应用程序,如下所示:

例子39. CSRF和JSON Spring MVC表单

1
2
3
4
<form action="https://bank.example.com/transfer.json" method="post" enctype="text/plain">
<input name='{"amount":100,"routingNumber":"evilsRoutingNumber","account":"evilsAccountNumber", "ignore_me":"' value='test"}' type='hidden'>
<input type="submit" value="Win Money!"/>
</form>
CSRF和无状态浏览器应用程序

如果我的应用程序是无状态的怎么办?这并不一定意味着您受到保护。实际上,如果用户不需要针对给定请求在Web浏览器中执行任何操作,则他们可能仍然容易受到CSRF攻击。

例如,考虑一个使用自定义cookie而不是JSESSIONID的应用程序,该自定义cookie包含其中的所有状态用于身份验证。进行CSRF攻击后,自定义cookie将与请求一起发送,其方式与在前面的示例中发送JSESSIONID cookie相同。此应用程序容易受到CSRF攻击。

使用基本身份验证的应用程序也容易受到CSRF攻击。该应用程序容易受到攻击,因为浏览器将在所有请求中自动包含用户名和密码,就像在前面的示例中发送JSESSIONID cookie一样。

CSRF注意事项

实施针对CSRF攻击的防护时,需要考虑一些特殊注意事项。

登录

为了防止伪造登录请求,应该保护HTTP请求中的登录免受CSRF攻击。必须防止伪造登录请求,以使恶意用户无法读取受害者的敏感信息。攻击执行如下:

  • 恶意用户使用恶意用户的凭据执行CSRF登录。现在,将受害者验证为恶意用户。
  • 然后,恶意用户诱骗受害者访问受感染的网站并输入敏感信息
  • 该信息与恶意用户的帐户相关联,因此恶意用户可以使用自己的凭据登录并查看vicitim的敏感信息

确保保护登录的HTTP请求免受CSRF攻击的可能麻烦在于,用户可能会遇到会话超时,从而导致请求被拒绝。会话超时对于不需要登录就需要会话的用户来说是令人惊讶的。有关更多信息,请参阅CSRF和会话超时

注销

为了防止伪造注销请求,应该保护注销HTTP请求免受CSRF攻击。必须防止伪造注销请求,以使恶意用户无法读取受害者的敏感信息。有关攻击的详细信息,请参阅此博客文章

确保注销HTTP请求免受CSRF攻击的可能并发症是用户可能会遇到会话超时,从而导致请求被拒绝。会话超时对于不希望需要会话才能注销的用户来说是令人惊讶的。有关更多信息,请参阅CSRF和会话超时

CSRF和会话超时

通常,预期的CSRF令牌存储在会话中。这意味着,会话期满后,服务器将不会找到预期的CSRF令牌并拒绝HTTP请求。有很多选项可以解决超时问题,每个选项都需要权衡取舍。

  • 减轻超时的最佳方法是使用JavaScript在表单提交时请求CSRF令牌。然后使用CSRF令牌更新该表单并提交。
  • 另一个选择是使用一些JavaScript,让用户知道他们的会话即将到期。用户可以单击一个按钮以继续并刷新会话。
  • 最后,预期的CSRF令牌可以存储在cookie中。这样可以使预期的CSRF令牌寿命更长。

    有人可能会问为什么默认情况下预期的CSRF令牌没有存储在Cookie中。这是因为存在已知的漏洞利用,其中另一个域可以设置标头(例如,用于指定cookie)。这与出现标题X-Requested-With时Ruby on Rails不再跳过CSRF检查的原因相同。请参阅此webappsec.org线程以获取有关如何执行漏洞利用的详细信息。另一个缺点是,通过删除状态(即超时),您将失去在令牌遭到破坏时强制使令牌无效的能力。

分段(文件上传)

保护分段请求(文件上传)免受CSRF攻击会导致鸡和蛋的问题。为了防止发生CSRF攻击,必须读取HTTP请求的正文以获得实际的CSRF令牌。但是,读取正文表示文件将被上传,这意味着外部站点可以上传文件。

将CSRF保护与multipart / form-data一起使用有两种选择。每个选项都有其取舍。

  • 将CSRF令牌放入Body
  • 将CSRF令牌放入URL

Note

在将Spring Security的CSRF保护与分段文件上传集成之前,请确保您可以首先在没有CSRF保护的情况下进行上传。有关在Spring中使用多部分表单的更多信息,请参见Spring参考的Multipart Resolver部分MultidocFilter javadoc

将CSRF令牌放入Body
第一种选择是在请求正文中包含实际的CSRF令牌。通过将CSRF令牌放入正文中,将在执行授权之前读取正文。这意味着任何人都可以在您的服务器上放置临时文件。但是,只有授权用户才能提交由您的应用程序处理的文件。通常,这是推荐的方法,因为临时文件上传对大多数服务器的影响可以忽略不计。

在网址中包含CSRF令牌
如果不允许未经授权的用户上传临时文件,则可以选择将预期的CSRF令牌作为查询参数包含在表单的action属性中。这种方法的缺点是查询参数可能会泄漏。更一般而言,将敏感数据放置在正文或标题中以确保不泄漏是最佳实践。可以在RFC 2616第15.1.3节“在URI中编码敏感信息”中找到其他信息。

HiddenHttpMethodFilter
在某些应用程序中,可以使用form参数来覆盖HTTP方法。例如,下面的表格可用于将HTTP方法视为删除而不是发布。

例子40. CSRF隐藏的HTTP方法表单

1
2
3
4
<form action="/process" method="post">
<!-- ... -->
<input type="hidden" name="_method" value="delete"/>
</form>

覆盖HTTP方法在过滤器中进行。该过滤器必须放在Spring Security支持之前。请注意,覆盖仅发生在POST上,因此实际上不太可能引起任何实际问题。但是,仍然最好的方法是确保将其放置在Spring Security的过滤器之前。

安全HTTP响应标头

Note

文档的此部分讨论安全HTTP响应标头的一般主题。有关安全HTTP响应标头Servlet和基于WebFlux的应用程序的特定信息,请参阅相关部分。

有许多HTTP响应标头可用于提高Web应用程序的安全性。本节专门介绍Spring Security提供显式支持的各种HTTP响应标头。如有必要,还可以将Spring Security配置为提供自定义标头

默认安全Headers

Note

请参阅相关部分,以了解如何为基于servlet和基于webflux的应用程序定制默认值。

Spring Security提供了一组默认的与安全性相关的HTTP响应标头,以提供安全的默认值。

Spring Security的默认值为包含以下标头:

例子41.默认的安全HTTP响应头

1
2
3
4
5
6
7
Cache-Control: no-cache, no-store, max-age=0, must-revalidate
Pragma: no-cache
Expires: 0
X-Content-Type-Options: nosniff
Strict-Transport-Security: max-age=31536000 ; includeSubDomains
X-Frame-Options: DENY
X-XSS-Protection: 1; mode=block

Note

仅在HTTPS请求上添加严格传输安全性

如果默认设置不能满足您的需要,则可以轻松地从这些默认设置中删除,修改或添加标题。 有关这些标题中的每个标题的更多详细信息,请参阅相应的部分:

缓存控制

Note

请参阅相关部分,以了解如何为基于servlet和基于webflux的应用程序定制默认值。

Spring Security的默认设置是禁用缓存以保护用户的内容。

如果用户通过身份验证可以查看敏感信息然后注销,则我们不希望恶意用户能够单击“后退”按钮查看敏感信息。 默认情况下发送的缓存控制标头是:

例子42.默认的缓存控制HTTP响应头

1
2
3
Cache-Control: no-cache, no-store, max-age=0, must-revalidate
Pragma: no-cache
Expires: 0

为了默认安全,Spring Security默认添加这些头。但是,如果您的应用程序提供了自己的缓存控制标头,Spring Security将会退出。这允许应用程序确保可以缓存CSS和JavaScript之类的静态资源。

内容类型选项

请参阅相关部分,以了解如何为基于servlet和基于webflux的应用程序定制默认值。

从历史上看,包括Internet Explorer在内的浏览器都会尝试使用内容嗅探来猜测请求的内容类型。这允许浏览器通过猜测未指定内容类型的资源上的内容类型来改善用户体验。例如,如果浏览器遇到一个未指定内容类型的JavaScript文件,它将能够猜测该内容类型然后运行它。

Note

允许上传内容时,还有许多其他事情(即,仅在不同的域中显示文档,确保设置了Content-Type标头,清理文档等)。但是,这些措施不在Spring Security提供的范围之内。同样重要的是要指出在禁用内容嗅探时,必须指定内容类型才能使内容正常工作。

内容嗅探的问题在于,这允许恶意用户使用多义标记(即,可以作为多种内容类型有效的文件)执行XSS攻击。例如,某些网站可能允许用户向网站提交有效的附言文档并进行查看。恶意用户可能会创建一个也是有效JavaScript文件的Postscript文档,并对其进行XSS攻击。

Spring Security通过在HTTP响应中添加以下标头来默认禁用内容嗅探:

例子43. nosniff HTTP响应头

1
X-Content-Type-Options: nosniff

HTTP严格传输安全性(HSTS)

Note

请参阅相关部分,以了解如何为基于servlet和基于webflux的应用程序定制默认值。

当您输入银行的网站时,您输入的是mybank.example.com还是输入https://mybank.example.com?如果省略https协议,则可能会受到[中间人攻击](https://en.wikipedia.org/wiki/Man-in-the-middle_attack)。即使网站执行了重定向到https://mybank.example.com的行为,恶意用户也可能拦截了初始HTTP请求并操纵了响应(例如,重定向到https://mibank.example.com并窃取了其凭据)。

许多用户忽略了https协议,这就是创建HTTP严格传输安全性(HSTS)的原因。将mybank.example.com添加为HSTS主机后,浏览器可以提前知道对mybank.example.com的任何请求都应解释为https://mybank.example.com。这大大降低了发生中间人攻击的可能性。

Note

根据RFC6797,HSTS标头仅注入HTTPS响应中。为了使浏览器能够确认标头,浏览器必须首先信任对用于建立连接的SSL证书(不仅仅是SSL证书)进行签名的CA。

将站点标记为HSTS主机的一种方法是将主机预加载到浏览器中。另一个是将Strict-Transport-Security标头添加到响应中。例如,Spring Security的默认行为是添加以下标头,该标头指示浏览器将域视为一年的HSTS主机(一年大约31536000秒):

例子44.严格的传输安全HTTP响应头

1
Strict-Transport-Security: max-age=31536000 ; includeSubDomains ; preload

可选的includeSubDomains指令可指示浏览器子域(例如secure.mybank.example.com)也应被视为HSTS域。

可选的preload指令指示浏览器将域作为HSTS域预加载到浏览器中。有关HSTS预加载的更多详细信息,请参见https://hstspreload.org

HTTP公钥固定(HPKP)

Note

为了保持被动,Spring Security仍在servlet环境中提供对HPKP的支持,但是由于上述原因,安全团队不再推荐HPKP。

HTTP公共密钥固定(HPKP)向Web客户端指定要与某些Web服务器一起使用的公共密钥,以防止使用伪造证书的中间人(MITM)攻击。如果使用得当,HPKP可以添加更多保护层,以防止受到破坏的证书。但是,由于HPKP的复杂性,许多专家不再建议使用它,Chrome甚至取消了对它的支持

有关为何不再推荐HPKP的更多详细信息,请阅读HTTP公钥固定已死吗?我放弃了HPKP

X-Frame-Options

Note

请参阅相关部分,以了解如何为基于servlet和基于webflux的应用程序定制默认值。

允许将您的网站添加到框架可能是一个安全问题。例如,使用聪明的CSS样式用户可能会被诱骗点击他们不想要的东西。例如,登录到其银行的用户可以单击将按钮授予其他用户访问权限。这种攻击称为Clickjacking

Note

处理点击劫持的另一种现代方法是使用内容安全策略(CSP)

有许多方法可以缓解点击劫持攻击。例如,要保护旧版浏览器免遭点击劫持攻击,可以使用分帧代码。虽然不完美,但是对于传统浏览器而言,破帧代码是最好的选择。

解决点击劫持的更现代方法是使用X-Frame-Options标头。默认情况下,Spring Security使用以下标头禁用iframe中的呈现页面:

1
X-Frame-Options: DENY

X-XSS-Protection

Note

请参阅相关部分,以了解如何为基于servlet和基于webflux的应用程序定制默认值。

一些浏览器内置了对过滤掉反射的XSS攻击)的支持。 这绝非万无一失,但确实有助于XSS保护。

通常默认情况下会启用过滤,因此添加标头通常只会确保标头已启用,并指示浏览器在检测到XSS攻击时应采取的措施。 例如,过滤器可能会尝试以最小侵入性的方式更改内容以仍然呈现所有内容。 有时,这种替换本身可能会成为XSS漏洞。 相反,最好是阻止内容,而不要尝试对其进行修复。 默认情况下,Spring Security使用以下标头阻止内容:

1
X-XSS-Protection: 1; mode=block

Content Security Policy (CSP)

Note

请参阅相关部分,以了解如何配置基于servlet和基于webflux的应用程序。

内容安全策略(CSP)是Web应用程序可以用来减轻诸如跨站点脚本(XSS)之类的内容注入漏洞的机制。 CSP是一种声明性策略,为Web应用程序作者提供了一种工具,可以声明该Web应用程序希望从中加载资源的来源,并最终将这些信息通知客户端(用户代理)。

Note

内容安全策略并非旨在解决所有内容注入漏洞。取而代之的是,可以利用CSP帮助减少内容注入攻击所造成的危害。作为第一道防线,Web应用程序作者应验证其输入并对输出进行编码。

Web应用程序可以通过在响应中包括以下HTTP标头之一来使用CSP:

  • Content-Security-Policy

  • Content-Security-Policy-Report-Only

这些标头中的每一个都用作将安全策略传递给客户端的机制。安全策略包含一组安全策略指令,每个指令负责声明对特定资源表示形式的限制。

例如,Web应用程序可以通过在响应中包括以下标头来声明它希望从特定的受信任源中加载脚本:

例子45.内容安全策略例子

1
Content-Security-Policy: script-src https://trustedscripts.example.com

违规报告是标准的JSON结构,可以由Web应用程序自己的API或公共托管的CSP违规报告服务(例如https://report-uri.io/)捕获。

Content-Security-Policy-Report-Only标头为Web应用程序作者和管理员提供了监视安全策略而不是强制执行这些策略的功能。 该标题通常在试验和/或开发站点的安全策略时使用。 当某个策略被认为有效时,可以通过使用Content-Security-Policy标头字段来强制实施。

给定以下响应头,该策略声明可以从两个可能的来源之一加载脚本。

例子47.仅内容安全策略报告

1
Content-Security-Policy-Report-Only: script-src 'self' https://trustedscripts.example.com; report-uri /csp-report-endpoint/

如果该站点违反了此策略,则通过尝试从evil.com加载脚本,用户代理会将违规报告发送到report-uri指令指定的声明URL,但是仍然允许违规资源加载。

将内容安全策略应用于Web应用程序通常是一项艰巨的任务。 以下资源可以为您的站点制定有效的安全策略提供进一步的帮助。

内容安全策略简介

CSP指南-Mozilla开发人员网络

W3C候选人推荐

Referrer Policy

Note

请参阅相关部分,以了解如何配置基于servlet和基于webflux的应用程序。

引荐来源网址政策是一种机制,Web应用程序可以利用该机制来管理引荐来源网址字段,该字段包含用户所在的最后一页。

Spring Security的方法是使用Referrer Policy标头,该标头提供了不同的策略

例子48.推荐人策略例子

1
Referrer-Policy: same-origin

Referrer-Policy响应标头指示浏览器让目的地知道用户先前所在的源。

功能政策

Note

请参阅相关部分,以了解如何配置基于servlet和基于webflux的应用程序。

功能策略是一种机制,它允许Web开发人员有选择地启用,禁用和修改浏览器中某些API和Web功能的行为。

例子49.功能策略例子

1
Feature-Policy: geolocation 'self'

借助功能策略,开发人员可以为浏览器选择一套“策略”,以实施整个站点中使用的特定功能。 这些政策限制了网站可以访问或修改某些功能的浏览器默认行为的API。

清除网站数据

Note

请参阅相关部分,以了解如何配置基于servlet和基于webflux的应用程序。

清除站点数据是一种机制,通过该机制,当HTTP响应包含以下标头时,可以删除所有浏览器端数据(cookie,本地存储等):

1
Clear-Site-Data: "cache", "cookies", "storage", "executionContexts"

这是注销时执行的不错的清理操作。

自定义标题

Note

请参阅相关部分,以了解如何配置两个基于Servlet的应用程序。

Spring Security具有使您可以方便地将更常见的安全标头添加到您的应用程序的机制。 但是,它也提供了挂钩来启用添加自定义标头。

HTTP

所有基于HTTP的通信,包括静态资源,都应使用TLS保护

作为一个框架,Spring Security不处理HTTP连接,因此不直接提供对HTTPS的支持。但是,它确实提供了许多有助于HTTPS使用的功能。

重定向到HTTPS

当客户端使用HTTP时,Spring Security可以配置为在Servlet和WebFlux环境中都重定向到HTTPS。

严格的传输安全

Spring Security提供对严格传输安全性的支持,并默认启用它。

代理服务器配置

使用代理服务器时,确保已正确配置应用程序很重要。例如,许多应用程序将具有负载平衡器,该负载平衡器通过将请求转发到位于https://192.168.1:8080的应用服务器来响应对https://example.com/的请求。知道负载均衡器存在,并将请求视为客户端请求https://192.168.1:8080。

要解决此问题,您可以使用RFC 7239指定正在使用负载平衡器。为了使应用程序意识到这一点,您需要配置应用程序服务器以了解X-Forwarded标头。例如,Tomcat使用RemoteIpValve,而Jetty使用ForwardedRequestCustomizer。另外,Spring用户可以利用ForwardedHeaderFilter

Spring Boot用户可以使用server.use-forward-headers属性配置应用程序。有关更多详细信息,请参见Spring Boot文档

项目模块和依赖

在Spring Security 3.0中,代码库被细分为单独的jar,这些jar更清楚地分离了不同的功能区域和第三方依赖项。 如果使用Maven构建项目,则应将这些模块添加到pom.xml中。 即使您不使用Maven,我们也建议您查阅pom.xml文件以了解第三方依赖关系和版本。 另一个好主意是检查示例应用程序中包含的库。

本部分提供了Spring Security中的模块以及它们在运行中的应用程序中运行所需要的其他依赖项的参考。我们不包括仅在构建或测试Spring Security本身时使用的依赖项。我们也没有包括外部依赖项所要求的传递性依赖项。

项目网站上列出了所需的Spring版本,因此下面的Spring依赖项省略了特定版本。请注意,Spring应用程序中的其他非安全功能可能仍需要下面列出为“可选”的某些依赖项。另外,如果大多数应用程序中都使用了列为“可选”的依赖项,则在项目的Maven POM文件中可能实际上并未将其标记为此类。除非您使用指定的功能,否则它们仅在不需要它们的意义上是“可选的”。

在模块依赖于另一个Spring Security模块的情况下,也假定该模块所依赖的模块的非可选依赖关系是必需的,因此未单独列出。

核心-spring-security-core.jar

该模块包含核心身份验证和访问控制类和接口,远程支持和基本配置API。 使用Spring Security的任何应用程序都需要它。 它支持独立的应用程序,远程客户端,方法(服务层)安全性和JDBC用户配置。 它包含以下顶级程序包:

  • org.springframework.security.core
  • org.springframework.security.access
  • org.springframework.security.authentication
  • org.springframework.security.provisioning

Table 1. Core Dependencies

Dependency Version Description
ehcache 1.6.2 Required if the Ehcache-based user cache implementation is used (optional).
spring-aop Method security is based on Spring AOP
spring-beans Required for Spring configuration
spring-expression Required for expression-based method security (optional)
spring-jdbc Required if using a database to store user data (optional).
spring-tx Required if using a database to store user data (optional).
aspectjrt 1.6.10 Required if using AspectJ support (optional).
jsr250-api 1.0 Required if you are using JSR-250 method-security annotations (optional).

Remoting-spring-security-remoting.jar

该模块提供了与Spring Remoting的集成。 除非您要编写使用Spring Remoting的远程客户端,否则您不需要这样做。 主要包是org.springframework.security.remoting

Dependency Version Description
spring-security-core
spring-web Required for clients which use HTTP remoting support.

Web-spring-security-web.jar

该模块包含过滤器和相关的Web安全基础结构代码。 它包含任何与Servlet API相关的内容。 如果您需要Spring Security Web认证服务和基于URL的访问控制,则需要它。 主要软件包是org.springframework.security.web

Dependency Version Description
spring-security-core
spring-web Spring web support classes are used extensively.
spring-jdbc Required for JDBC-based persistent remember-me token repository (optional).
spring-tx Required by remember-me persistent token repository implementations (optional).

配置-spring-security-config.jar

该模块包含安全名称空间解析代码和Java配置代码。 如果您使用Spring Security XML名称空间进行配置或Spring Security的Java配置支持,则需要它。 主要软件包是org.springframework.security.config。 这些类都不打算直接在应用程序中使用。

Dependency Version Description
spring-security-core
spring-security-web Required if you are using any web-related namespace configuration (optional).
spring-security-ldap Required if you are using the LDAP namespace options (optional).
spring-security-openid Required if you are using OpenID authentication (optional).
aspectjweaver 1.6.10 Required if using the protect-pointcut namespace syntax (optional).

LDAP-spring-security-ldap.jar

该模块提供LDAP身份验证和供应代码。 如果您需要使用LDAP认证或管理LDAP用户条目,则为必填项。 顶级包是org.springframework.security.ldap

Dependency Version Description
spring-security-core
spring-ldap-core 1.3.0 LDAP support is based on Spring LDAP.
spring-tx Data exception classes are required.
apache-ds [1] 1.5.5 Required if you are using an embedded LDAP server (optional).
shared-ldap 0.9.15 Required if you are using an embedded LDAP server (optional).
ldapsdk 4.1 Mozilla LdapSDK. Used for decoding LDAP password policy controls if you are using password-policy functionality with OpenLDAP, for example.

OAuth 2.0 Core-spring-security-oauth2-core.jar

spring-security-oauth2-core.jar包含核心类和接口,这些类和接口提供对OAuth 2.0授权框架和OpenID Connect Core 1.0的支持。 使用OAuth 2.0或OpenID Connect Core 1.0的应用程序(例如客户端,资源服务器和授权服务器)需要它。 顶级软件包是org.springframework.security.oauth2.core

OAuth 2.0 Client-spring-security-oauth2-client.jar

spring-security-oauth2-client.jar包含Spring Security对OAuth 2.0授权框架和OpenID Connect Core 1.0的客户端支持。 使用OAuth 2.0登录或OAuth客户端支持的应用程序需要使用它。 顶级软件包是org.springframework.security.oauth2.client

OAuth 2.0 JOSE — spring-security-oauth2-jose.jar

spring-security-oauth2-jose.jar包含Spring Security对JOSE(Javascript对象签名和加密)框架的支持。 JOSE框架旨在提供一种在各方之间安全地转移声明的方法。 它是根据一系列规范构建的:

  • JSON Web Token (JWT)
  • JSON Web Signature (JWS)
  • JSON Web Encryption (JWE)
  • JSON Web Key (JWK)

包括以下的顶级软件包:

  • org.springframework.security.oauth2.jwt
  • org.springframework.security.oauth2.jose

OAuth 2.0 Resource Server — spring-security-oauth2-resource-server.jar

spring-security-oauth2-resource-server.jar包含Spring Security对OAuth 2.0资源服务器的支持。 它用于通过OAuth 2.0承载令牌保护API。 顶级软件包是org.springframework.security.oauth2.server.resource

ACL — spring-security-acl.jar

该模块包含专门的域对象ACL实现。 它用于将安全性应用于应用程序中的特定域对象实例。 顶级包是org.springframework.security.acls

Dependency Version Description
spring-security-core
ehcache 1.6.2 如果使用基于Ehcache的ACL缓存实现,则为必需(如果使用自己的实现,则为可选)。
spring-jdbc 如果使用的是默认的基于JDBC的AclService,则为必需(如果实现自己的,则为可选)。
spring-tx 如果使用的是默认的基于JDBC的AclService,则为必需(如果实现自己的,则为可选)。

CAS — spring-security-cas.jar

该模块包含Spring Security的CAS客户端集成。 如果要对CAS单点登录服务器使用Spring Security Web认证,则应该使用它。 顶级包是org.springframework.security.cas

Dependency Version Description
spring-security-core
spring-security-web
cas-client-core 3.1.12 The JA-SIG CAS Client. This is the basis of the Spring Security integration.
ehcache 1.6.2 Required if you are using the Ehcache-based ticket cache (optional).

OpenID — spring-security-openid.jar

Note

已弃用OpenID 1.0和2.0协议,并鼓励用户迁移到Spring-security-oauth2支持的OpenID Connect。

此模块包含OpenID Web身份验证支持。 它用于根据外部OpenID服务器对用户进行身份验证。 顶级包是org.springframework.security.openid。 它需要OpenID4Java。

Dependency Version Description
spring-security-core
spring-security-web
openid4java-nodeps 0.9.6 Spring Security’s OpenID integration uses OpenID4Java.
httpclient 4.1.1 openid4java-nodeps depends on HttpClient 4.
guice 2.0 openid4java-nodeps depends on Guice 2.

Test — spring-security-test.jar

该模块包含对使用Spring Security进行测试的支持。

Taglibs — spring-secuity-taglibs.jar

提供Spring Security的JSP标签实现。

Dependency Version Description
spring-security-core
spring-security-web
spring-security-acl Required if you are using the accesscontrollist tag or hasPermission() expressions with ACLs (optional).
spring-expression Required if you are using SPEL expressions in your tag access constraints.

Samples

Spring Security包含许多示例应用程序

Servlet Applications

Spring Security通过使用标准的Servlet过滤器与Servlet容器集成。 这意味着它可以与在Servlet容器中运行的任何应用程序一起使用。 更具体地说,您无需在基于Servlet的应用程序中使用Spring即可利用Spring Security。

servlet-hello

Hello Spring Security

本节介绍了如何将Spring Security与Spring Boot结合使用的最小设置。

Note

完整的应用程序可以在sample/boot/helloworld中找到。为方便起见,您可以通过单击此处下载最小的Spring Boot + Spring Security应用程序。

更新依赖项

您唯一需要做的步骤就是使用MavenGradle更新依赖关系。

启动Hello Spring安全启动

现在,您可以使用Maven插件的运行目标来运行Spring Boot应用程序。 以下示例显示了如何执行此操作(以及执行此操作的输出的开头):

例子50.运行Spring Boot应用程序

1
2
3
4
5
6
7
$ ./mvn spring-boot:run
...
INFO 23689 --- [ restartedMain] .s.s.UserDetailsServiceAutoConfiguration :

Using generated security password: 8e557245-73e2-4286-969a-ff57fe326336

...

Spring Boot自动配置

Spring Boot自动:

  • 启用Spring Security的默认配置,该配置将Servlet过滤器创建为名为springSecurityFilterChain的bean。该bean负责应用程序内的所有安全性(保护应用程序URL,验证提交的用户名和密码,重定向到登录表单等)。
  • 创建一个UserDetailsService Bean,其中包含用户名user和随机生成的密码,并记录到控制台。
  • 针对每个请求,使用Servlet容器向名为springSecurityFilterChain的bean注册过滤器。

Spring Boot的配置不多,但功能很多。功能摘要如下:

Servlet安全性:大局

本节讨论基于Servlet的应用程序中Spring Security的高级体系结构。 我们在参考的“身份验证授权防止利用漏洞”部分中建立了这种高级理解。

筛选器的回顾

Spring Security的Servlet支持基于Servlet过滤器,因此通常首先了解过滤器的作用会很有帮助。 下图显示了单个HTTP请求的处理程序的典型分层。

filterchain

Figure 1. FilterChain

客户端向应用程序发送请求,然后容器创建一个FilterChain,其中包含应基于请求URI的路径处理HttpServletRequest的过滤器和Servlet。 在Spring MVC应用程序中,Servlet是DispatcherServlet的实例。 一个Servlet最多只能处理一个HttpServletRequest和HttpServletResponse。 但是,可以使用多个过滤器来:

  • 防止下游过滤器或Servlet被调用。 在这种情况下,过滤器通常将编写HttpServletResponse。
  • 修改下游过滤器和Servlet使用的HttpServletRequestHttpServletResponse

过滤器的功能来自传递给它的FilterChain

Example 47. FilterChain Usage Example

1
2
3
4
5
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) {
// do something before the rest of the application
chain.doFilter(request, response); // invoke the rest of the application
// do something after the rest of the application
}
1
2
3
4
5
fun doFilter(request: ServletRequest, response: ServletResponse, chain: FilterChain) {
// do something before the rest of the application
chain.doFilter(request, response) // invoke the rest of the application
// do something after the rest of the application
}

由于过滤器仅影响下游过滤器和Servlet,因此调用每个过滤器的顺序非常重要。

DelegatingFilterProxy

Spring提供了一个名为DelegatingFilterProxy的Filter实现,可以在Servlet容器的生命周期和Spring的ApplicationContext之间进行桥接。 Servlet容器允许使用其自己的标准注册Filters,但它不了解Spring定义的Bean。 DelegatingFilterProxy可以通过标准的Servlet容器机制进行注册,但是可以将所有工作委托给实现Filter的Spring Bean。

这是DelegatingFilterProxy如何适合Filters和FilterChain的图片。

DelegatingFilterProxyApplicationContext查找Bean Filter0,然后调用Bean Filter0。 DelegatingFilterProxy的伪代码如下所示。

1
2
3
4
5
6
7
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) {
// Lazily get Filter that was registered as a Spring Bean
// For the example in DelegatingFilterProxy delegate is an instance of Bean Filter0
Filter delegate = getFilterBean(someBeanName);
// delegate work to the Spring Bean
delegate.doFilter(request, response);
}
1
2
3
4
5
6
7
fun doFilter(request: ServletRequest, response: ServletResponse, chain: FilterChain) {
// Lazily get Filter that was registered as a Spring Bean
// For the example in DelegatingFilterProxy delegate is an instance of Bean Filter0
val delegate: Filter = getFilterBean(someBeanName)
// delegate work to the Spring Bean
delegate.doFilter(request, response)
}

DelegatingFilterProxy的另一个好处是它允许延迟查找Filter bean实例。 这很重要,因为容器需要在启动容器之前注册Filter实例。 但是,Spring通常使用ContextLoaderListener加载Spring Bean,直到需要注册Filter实例之后才可以加载。

FilterChainProxy

Spring Security的Servlet支持包含在FilterChainProxy中。 FilterChainProxy是Spring Security提供的特殊过滤器,允许通过SecurityFilterChain委派许多过滤器实例。 由于FilterChainProxy是Bean,因此通常将其包装在DelegatingFilterProxy中。

SecurityFilterChain

FilterChainProxy使用SecurityFilterChain来确定应对此请求调用哪些Spring Security过滤器。

SecurityFilterChain中的安全过滤器通常是Bean,但它们是使用FilterChainProxy而不是DelegatingFilterProxy注册的。 FilterChainProxy为直接向Servlet容器或DelegatingFilterProxy注册提供了许多优势。首先,它为Spring Security的所有Servlet支持提供了一个起点。因此,如果您想对Spring Security的Servlet支持进行故障排除,那么在FilterChainProxy中添加调试点是一个很好的起点。

其次,由于FilterChainProxy对于Spring Security的使用至关重要,因此它可以执行不被视为可选任务的任务。例如,它清除SecurityContext以避免内存泄漏。它还使用Spring Security的HttpFirewall来保护应用程序免受某些类型的攻击。

此外,它在确定何时应调用SecurityFilterChain时提供了更大的灵活性。在Servlet容器中,仅根据URL调用过滤器。但是,FilterChainProxy可以利用RequestMatcher接口,根据HttpServletRequest中的任何内容确定调用。

实际上,FilterChainProxy可用于确定应使用哪个SecurityFilterChain。这允许为您的应用程序的不同部分提供完全独立的配置。

在“多个SecurityFilterChain”图中,FilterChainProxy决定应使用哪个SecurityFilterChain。仅匹配的第一个SecurityFilterChain将被调用。如果请求的URL是/api/messages/,则它将首先与SecurityFilterChain0/api/** 模式匹配,因此即使SecurityFilterChain0也与SecurityFilterChainn匹配,也只会调用SecurityFilterChain0。如果请求的URL是/messages/,则与SecurityFilterChain0/api/**模式不匹配,因此FilterChainProxy将继续尝试每个SecurityFilterChain。假设没有其他匹配SecurityFilterChainnSecurityFilterChain实例将被调用。

请注意,SecurityFilterChain0仅配置了三个安全筛选器实例。但是,SecurityFilterChainn配置了四个安全筛选器。重要的是要注意,每个SecurityFilterChain可以是唯一的,并且可以独立配置。实际上,如果应用程序希望Spring Security忽略某些请求,则SecurityFilterChain可能具有零个安全过滤器。

Security Filters

安全过滤器通过SecurityFilterChain API插入到FilterChainProxy中。 过滤器的顺序很重要。 通常不必知道Spring Security的过滤器的顺序。 但是,有时候知道顺序是有益的

以下是Spring Security过滤器顺序的完整列表:

  • ChannelProcessingFilter
  • WebAsyncManagerIntegrationFilter
  • SecurityContextPersistenceFilter
  • HeaderWriterFilter
  • CorsFilter
  • CsrfFilter
  • LogoutFilter
  • OAuth2AuthorizationRequestRedirectFilter
  • Saml2WebSsoAuthenticationRequestFilter
  • X509AuthenticationFilter
  • AbstractPreAuthenticatedProcessingFilter
  • CasAuthenticationFilter
  • OAuth2LoginAuthenticationFilter
  • Saml2WebSsoAuthenticationFilter
  • UsernamePasswordAuthenticationFilter
  • OpenIDAuthenticationFilter
  • DefaultLoginPageGeneratingFilter
  • DefaultLogoutPageGeneratingFilter
  • ConcurrentSessionFilter
  • DigestAuthenticationFilter
  • BearerTokenAuthenticationFilter
  • BasicAuthenticationFilter
  • RequestCacheAwareFilter
  • SecurityContextHolderAwareRequestFilter
  • JaasApiIntegrationFilter
  • RememberMeAuthenticationFilter
  • AnonymousAuthenticationFilter
  • OAuth2AuthorizationCodeGrantFilter
  • SessionManagementFilter
  • ExceptionTranslationFilter
  • FilterSecurityInterceptor
  • SwitchUserFilter

Handling Security Exceptions

ExceptionTranslationFilter允许将AccessDeniedExceptionAuthenticationException转换为HTTP响应。

ExceptionTranslationFilter作为安全筛选器之一插入到FilterChainProxy中。

  1. 首先,ExceptionTranslationFilter调用FilterChain.doFilter(request,response)来调用应用程序的其余部分。

  2. 如果用户未通过身份验证或它是AuthenticationException,则启动身份验证。

    • 已清除SecurityContextHolder

    • HttpServletRequest保存在RequestCache中。 用户成功进行身份验证后,将使用RequestCache重播原始请求。

    • AuthenticationEntryPoint用于从客户端请求凭据。 例如,它可能重定向到登录页面或发送WWW-Authenticate标头。

  3. 否则,如果它是AccessDeniedException,则拒绝访问。 调用AccessDeniedHandler以处理拒绝的访问。

如果应用程序未引发AccessDeniedExceptionAuthenticationException,则ExceptionTranslationFilter不执行任何操作。

ExceptionTranslationFilter的伪代码如下所示:

1
2
3
4
5
6
7
8
9
try {
filterChain.doFilter(request, response); 1
} catch (AccessDeniedException | AuthenticationException ex) {
if (!authenticated || ex instanceof AuthenticationException) {
startAuthentication(); 2
} else {
accessDenied(); 3
}
}
  1. 您会从“筛选器回顾”中回顾到,调用FilterChain.doFilter(request,response)等同于调用应用程序的其余部分。
    这意味着如果应用程序的另一部分(即FilterSecurityInterceptor或方法安全性)引发AuthenticationExceptionAccessDeniedException,则会在此处捕获并处理。
  2. 如果用户未通过身份验证或它是AuthenticationException,则启动身份验证。
  3. 否则,访问被拒绝

认证

Spring Security提供了对身份验证的全面支持。 本节讨论:

架构组件

本部分描述了Servlet身份验证中使用的Spring Security的主要架构组件。如果您需要解释这些部分如何组合的具体流程,请查看“身份验证机制”特定部分。

认证机制

SecurityContextHolder

Spring Security身份验证模型的核心是SecurityContextHolder。 它包含SecurityContext

Spring Security在其中存储SecurityContextHolder,用于存储通过身份验证的人员的详细信息。 Spring Security并不关心如何填充SecurityContextHolder。 如果它包含一个值,那么它将用作当前经过身份验证的用户。

指示用户已通过身份验证的最简单方法是直接设置SecurityContextHolder

例子53.设置SecurityContextHolder

1
2
3
4
5
6
SecurityContext context = SecurityContextHolder.createEmptyContext(); 
Authentication authentication =
new TestingAuthenticationToken("username", "password", "ROLE_USER");
context.setAuthentication(authentication);

SecurityContextHolder.setContext(context);
1
2
3
4
5
val context: SecurityContext = SecurityContextHolder.createEmptyContext() 
val authentication: Authentication = TestingAuthenticationToken("username", "password", "ROLE_USER")
context.authentication = authentication

SecurityContextHolder.setContext(context)
  1. 我们首先创建一个空的SecurityContext
    重要的是创建一个新的SecurityContext实例,而不是使用SecurityContextHolder.getContext().setAuthentication(authentication)以避免跨多个线程的竞争条件。
  2. 接下来,我们创建一个新的身份验证对象。
    Spring Security并不关心在SecurityContext上设置什么类型的Authentication实现。
    这里我们使用TestingAuthenticationToken,因为它非常简单。
    一个更常见的生产方案是UsernamePasswordAuthenticationToken(userDetails,password,authorities)
  3. 最后,我们在SecurityContextHolder上设置SecurityContext。
    Spring Security将使用此信息进行授权。
    如果希望获取有关已验证主体的信息,可以通过访问SecurityContextHolder来获得。

例子54.访问当前认证的用户

1
2
3
4
5
SecurityContext context = SecurityContextHolder.getContext();
Authentication authentication = context.getAuthentication();
String username = authentication.getName();
Object principal = authentication.getPrincipal();
Collection<? extends GrantedAuthority> authorities = authentication.getAuthorities();
1
2
3
4
5
val context = SecurityContextHolder.getContext()
val authentication = context.authentication
val username = authentication.name
val principal = authentication.principal
val authorities = authentication.authorities

默认情况下,SecurityContextHolder使用ThreadLocal存储这些详细信息,这意味着即使没有将SecurityContext作为这些方法的参数显式传递,SecurityContext始终可用于同一线程中的方法。如果在处理当前委托人的请求之后要清除线程,则以这种方式使用ThreadLocal是非常安全的。 Spring Security的FilterChainProxy确保始终清除SecurityContext

由于某些应用程序使用线程的特定方式,因此它们并不完全适合使用ThreadLocal。例如,Swing客户端可能希望Java虚拟机中的所有线程都使用相同的安全上下文。可以在启动时为SecurityContextHolder配置策略,以指定您希望如何存储上下文。对于独立的应用程序,您将使用SecurityContextHolder.MODE_GLOBAL策略。其他应用程序可能希望让安全线程产生的线程也采用相同的安全身份。这可以通过使用SecurityContextHolder.MODE_INHERITABLETHREADLOCAL来实现。您可以通过两种方式从默认的SecurityContextHolder.MODE_THREADLOCAL更改模式。第一个是设置系统属性,第二个是在SecurityContextHolder上调用静态方法。大多数应用程序不需要更改默认值,但是如果需要,请查看JavaDoc for SecurityContextHolder以了解更多信息。

SecurityContext

从SecurityContextHolder获得SecurityContextSecurityContext包含一个身份验证对象。

Authentication

身份验证在Spring Security中有两个主要用途:

  • AuthenticationManager的输入,用于提供用户提供的用于身份验证的凭据。 在这种情况下使用时,isAuthenticated()返回false
  • 代表当前经过身份验证的用户。 可以从SecurityContext获得当前的身份验证。

身份验证包含:

  • principal-标识用户。 使用用户名/密码进行身份验证时,这通常是UserDetails的实例。
  • credentials-通常是密码。 在许多情况下,将在验证用户身份后清除此内容,以确保它不会泄漏。

  • authorities-GrantedAuthority是授予用户的高级权限。 角色或范围是几个例子。

GrantedAuthority

GrantedAuthoritys是授予用户的高级权限。角色或范围是几个例子。

可以从Authentication.getAuthorities()方法获得GrantedAuthoritys。此方法提供了GrantedAuthority对象的集合。毫不奇怪,GrantedAuthority是授予主体的权限。此类权限通常是“角色”,例如ROLE_ADMINISTRATORROLE_HR_SUPERVISOR。稍后将这些角色配置为Web授权,方法授权和域对象授权。 Spring Security的其他部分能够解释这些权限,并希望它们存在。使用基于用户名/密码的身份验证时,GrantedAuthority通常由UserDetailsService加载。

通常,GrantedAuthority对象是应用程序范围的权限。它们不特定于给定的域对象。因此,您不太可能具有GrantedAuthority来表示对Employee对象编号54的许可,因为如果有成千上万个这样的权限,您很快就会用完内存(或者至少导致应用程序花费很长时间)时间来认证用户)。当然,Spring Security是专门为满足这一通用要求而设计的,但您可以为此目的使用项目的域对象安全功能。

AuthenticationManager

AuthenticationManager是用于定义Spring Security的过滤器如何执行身份验证的API。 然后由调用AuthenticationManager的控制器(即Spring Security的Filters)在SecurityContextHolder上设置返回的身份验证。 如果您不与Spring Security的过滤器集成,则可以直接设置SecurityContextHolder,并且不需要使用AuthenticationManager

虽然AuthenticationManager的实现可以是任何东西,但最常见的实现是ProviderManager

ProviderManager

ProviderManager是AuthenticationManager的最常用实现。 ProviderManager委托给AuthenticationProviders列表。 每个AuthenticationProvider都有机会指示认证应该成功,失败,或者表明它不能做出决定并允许下游AuthenticationProvider进行决定。 如果没有一个已配置的AuthenticationProviders可以进行身份验证,则身份验证将失败,并显示ProviderNotFoundException,这是一个特殊的AuthenticationException,它指示ProviderManager未配置为支持传递给它的Authentication类型。

实际上,每个AuthenticationProvider都知道如何执行特定类型的身份验证。 例如,一个AuthenticationProvider可能能够验证用户名/密码,而另一个可能能够验证SAML断言。 这允许每个AuthenticationProvider进行非常特定类型的身份验证,同时支持多种身份验证,并且仅公开一个AuthenticationManager bean。

ProviderManager还允许配置可选的父AuthenticationManager,如果没有AuthenticationProvider可以执行身份验证,请咨询该父项。 父级可以是任何类型的AuthenticationManager,但通常是ProviderManager的实例。

实际上,多个ProviderManager实例可能共享同一个父AuthenticationManager。 在存在多个具有相同身份验证(共享父AuthenticationManager)但又具有不同身份验证机制(不同ProviderManager实例)的多个SecurityFilterChain实例的情况下,这种情况有些常见。

默认情况下,ProviderManager会尝试从身份验证对象中清除所有由成功身份验证请求返回的敏感凭据信息。这样可以防止将密码之类的信息保留在HttpSession中所需的时间。

例如,在使用用户对象的缓存来提高无状态应用程序的性能时,这可能会导致问题。如果身份验证包含对缓存中某个对象(例如UserDetails实例)的引用,并且已删除其凭据,则将无法再对缓存的值进行身份验证。如果使用缓存,则需要考虑到这一点。一个明显的解决方案是首先在高速缓存实现中或在创建返回的Authentication对象的AuthenticationProvider中创建对象的副本。或者,您可以在ProviderManager上禁用deleteCredentialsAfterAuthentication属性。有关更多信息,请参见Javadoc

AuthenticationProvider

可以将多个AuthenticationProviders注入ProviderManager。 每个AuthenticationProvider执行特定的身份验证类型。 例如,DaoAuthenticationProvider支持基于用户名/密码的身份验证,而JwtAuthenticationProvider支持对JWT令牌的身份验证。

Request Credentials with AuthenticationEntryPoint

AuthenticationEntryPoint用于发送HTTP响应,以从客户端请求凭据。

有时,客户端会主动包含凭据(例如用户名/密码)以请求资源。 在这些情况下,Spring Security无需提供HTTP响应即可从客户端请求凭证,因为它们已包含在内。

在其他情况下,客户端将对未经授权访问的资源发出未经身份验证的请求。 在这种情况下,AuthenticationEntryPoint的实现用于从客户端请求凭据。 AuthenticationEntryPoint实现可能会执行重定向到登录页面,使用WWW-Authenticate标头进行响应等。

AbstractAuthenticationProcessingFilter

AbstractAuthenticationProcessingFilter用作基础过滤器,用于验证用户的凭据。 在对凭证进行身份验证之前,Spring Security通常使用AuthenticationEntryPoint请求凭证。

接下来,AbstractAuthenticationProcessingFilter可以对提交给它的任何身份验证请求进行身份验证。

  1. 当用户提交其凭据时,AbstractAuthenticationProcessingFilter将从要验证的HttpServletRequest创建一个Authentication。创建的身份验证类型取决于AbstractAuthenticationProcessingFilter的子类。例如,UsernamePasswordAuthenticationFilter根据在HttpServletRequest中提交的用户名和密码创建UsernamePasswordAuthenticationToken

  2. 接下来,将身份验证传递到AuthenticationManager进行身份验证。

  3. 如果身份验证失败,则失败

    • SecurityContextHolder已清除。
    • RememberMeServices.loginFail被调用。如果记得我没有配置,这是一个禁忌。
    • AuthenticationFailureHandler被调用。
  4. 如果身份验证成功,则为成功。
  • 新的登录通知SessionAuthenticationStrategy

  • 身份验证是在SecurityContextHolder上设置的。稍后,SecurityContextPersistenceFilterSecurityContext保存到HttpSession中。

  • RememberMeServices.loginSuccess被调用。如果记得我没有配置,这是一个禁忌。
  • ApplicationEventPublisher发布一个InteractiveAuthenticationSuccessEvent
  • AuthenticationSuccessHandler被调用。

Username/Password Authentication

验证用户身份的最常见方法之一是验证用户名和密码。 这样,Spring Security为使用用户名和密码进行身份验证提供了全面的支持。

读取用户名和密码

Spring Security提供了以下内置机制,用于从HttpServletRequest中读取用户名和密码:

储存机制

用于读取用户名和密码的每种受支持的机制都可以利用任何受支持的存储机制:

表单登录

Spring Security提供对通过html表单提供的用户名和密码的支持。 本节提供有关基于表单的身份验证在Spring Security中如何工作的详细信息。

让我们看看Spring Security中基于表单的登录如何工作。 首先,我们了解如何将用户重定向到登录表单。

该图基于我们的SecurityFilterChain图。

  1. 首先,用户对未经授权的资源/private进行未经身份验证的请求。

  2. Spring Security的FilterSecurityInterceptor表示通过抛出AccessDeniedException拒绝了未经身份验证的请求。

  3. 由于未对用户进行身份验证,因此ExceptionTranslationFilter会启动“开始身份验证”,并使用配置的AuthenticationEntryPoint将重定向发送到登录页面。在大多数情况下,AuthenticationEntryPointLoginUrlAuthenticationEntryPoint的实例。

  4. 然后,浏览器将请求将其重定向到的登录页面。

  5. 应用程序中的某些内容必须呈现登录页面

提交用户名和密码后,UsernamePasswordAuthenticationFilter会对用户名和密码进行身份验证。 UsernamePasswordAuthenticationFilter扩展了AbstractAuthenticationProcessingFilter,因此此图看起来应该非常相似。

图7.验证用户名和密码
该图基于我们的SecurityFilterChain图。

  1. 当用户提交其用户名和密码时,UsernamePasswordAuthenticationFilter通过从HttpServletRequest中提取用户名和密码来创建UsernamePasswordAuthenticationToken,这是一种身份验证类型。

  2. 接下来,将UsernamePasswordAuthenticationToken传递到AuthenticationManager进行身份验证。 AuthenticationManager外观的详细信息取决于用户信息的存储方式

  3. 如果身份验证失败,则失败

  • SecurityContextHolder已清除。

  • RememberMeServices.loginFail被调用。如果记得我没有配置,这是一个禁忌。

  • AuthenticationFailureHandler被调用。

  1. 如果身份验证成功,则为成功。
  • 新的登录通知SessionAuthenticationStrategy

  • 身份验证是在SecurityContextHolder上设置的。

  • RememberMeServices.loginSuccess被调用。如果记得我没有配置,这是一个禁忌。

  • ApplicationEventPublisher发布一个InteractiveAuthenticationSuccessEvent

  • AuthenticationSuccessHandler被调用。通常,这是一个SimpleUrlAuthenticationSuccessHandler,当我们重定向到登录页面时,它将重定向到ExceptionTranslationFilter保存的请求。

默认情况下,Spring Security表单登录处于启用状态。但是,一旦提供了任何基于servlet的配置,就必须显式提供基于表单的登录。可以在下面找到最小的显式Java配置:

例子55.表单登录

1
2
3
4
5
protected void configure(HttpSecurity http) {
http
// ...
.formLogin(withDefaults());
}
1
2
3
4
<http>
<!-- ... -->
<form-login />
</http>
1
2
3
4
5
6
fun configure(http: HttpSecurity) {
http {
// ...
formLogin { }
}
}

在这种配置中,Spring Security将呈现一个默认的登录页面。 大多数生产应用程序将需要自定义登录表单。

下面的配置演示了如何提供自定义登录表单。

例子56.定制登录表单配置

1
2
3
4
5
6
7
8
protected void configure(HttpSecurity http) throws Exception {
http
// ...
.formLogin(form -> form
.loginPage("/login")
.permitAll()
);
}
1
2
3
4
5
<http>
<!-- ... -->
<intercept-url pattern="/login" access="permitAll" />
<form-login login-page="/login" />
</http>
1
2
3
4
5
6
7
8
9
fun configure(http: HttpSecurity) {
http {
// ...
formLogin {
loginPage = "/login"
permitAll()
}
}
}

在Spring Security配置中指定登录页面后,您将负责呈现该页面。 以下是Thymeleaf模板,该模板生成符合/ login登录页面的HTML登录表单。

例子57.登录表单
src/main/resources/templates/login.html

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:th="https://www.thymeleaf.org">
<head>
<title>Please Log In</title>
</head>
<body>
<h1>Please Log In</h1>
<div th:if="${param.error}">
Invalid username and password.</div>
<div th:if="${param.logout}">
You have been logged out.</div>
<form th:action="@{/login}" method="post">
<div>
<input type="text" name="username" placeholder="Username"/>
</div>
<div>
<input type="password" name="password" placeholder="Password"/>
</div>
<input type="submit" value="Log in" />
</form>
</body>
</html>

关于默认HTML表单,有一些关键点:

  • 该表格应post/login

  • 该表格将需要包含一个由Thymeleaf自动包含CSRF令牌

  • 该表单应在名为username的参数中指定用户名

  • 该表格应在名为password的参数中指定密码

  • 如果找到HTTP参数错误,则表明用户未能提供有效的用户名/密码

  • 如果找到HTTP参数注销,则表明用户已成功注销

除了自定义登录页面外,许多用户将不需要更多。 但是,如果需要,可以使用其他配置自定义以上所有内容。

如果您使用的是Spring MVC,则需要一个将GET /login映射到我们创建的登录模板的控制器。 下面是最小的LoginController示例:

例子58. LoginController
src/main/java/example/LoginController.java

1
2
3
4
5
6
7
@Controller
class LoginController {
@GetMapping("/login")
String login() {
return "login";
}
}

基本身份验证

本节详细介绍了Spring Security如何为基于servlet的应用程序提供对基本HTTP身份验证的支持。

让我们看一下HTTP基本身份验证在Spring Security中如何工作。 首先,我们看到WWW-Authenticate标头被发送回未经身份验证的客户端。

图8.发送WWW验证头
该图基于我们的SecurityFilterChain图。

  1. 首先,用户对未经授权的资源/ private进行未经身份验证的请求。

  2. Spring Security的FilterSecurityInterceptor表示通过抛出AccessDeniedException拒绝了未经身份验证的请求。

  3. 由于用户未通过身份验证,因此ExceptionTranslationFilter会启动“开始身份验证”。 配置的AuthenticationEntryPointBasicAuthenticationEntryPoint的实例,该实例发送WWW-Authenticate标头。 RequestCache通常是一个NullRequestCache,它不保存请求,因为客户端能够重播它最初请求的请求。

当客户端收到WWW-Authenticate标头时,它知道应该使用用户名和密码重试。 以下是正在处理的用户名和密码的流程。

该图基于我们的SecurityFilterChain图。

  1. 当用户提交其用户名和密码时,BasicAuthenticationFilter通过从HttpServletRequest中提取用户名和密码来创建UsernamePasswordAuthenticationToken,这是一种身份验证类型。

  2. 接下来,将UsernamePasswordAuthenticationToken传递到AuthenticationManager进行身份验证。 AuthenticationManager外观的详细信息取决于用户信息的存储方式

  3. 如果身份验证失败,则失败

  • SecurityContextHolder已清除。

  • RememberMeServices.loginFail被调用。如果记得我没有配置,这是一个禁忌。

  • AuthenticationEntryPoint被调用以触发WWW-Authenticate重新发送。

  1. 如果身份验证成功,则为成功。
  • 身份验证是在SecurityContextHolder上设置的。

  • RememberMeServices.loginSuccess被调用。如果记得我没有配置,这是一个禁忌。

  • BasicAuthenticationFilter调用FilterChain.doFilter(request,response)继续进行其余的应用程序逻辑。

Spring Security的HTTP基本身份验证支持默认为启用。但是,一旦提供了任何基于servlet的配置,就必须显式提供HTTP Basic。

最小的显式配置可以在下面找到:

例子59.显式HTTP基本配置

1
2
3
4
5
protected void configure(HttpSecurity http) {
http
// ...
.httpBasic(withDefaults());
}
1
2
3
4
<http>
<!-- ... -->
<http-basic />
</http>
1
2
3
4
5
6
fun configure(http: HttpSecurity) {
http {
// ...
httpBasic { }
}
}

摘要身份验证

本节详细介绍了Spring Security如何为DigestAuthenticationFilter提供的Digest Authentication支持。

Note

您不应该在现代应用程序中使用摘要式身份验证,因为它不安全。最明显的问题是您必须以纯文本,加密或MD5格式存储密码。所有这些存储格式都被认为是不安全的。相反,您应该使用摘要式身份验证不支持的一种单向自适应密码哈希(即bCrypt,PBKDF2,SCrypt等)存储凭据。

摘要式身份验证试图解决基本身份验证的许多弱点,特别是通过确保凭据不会以明文形式通过网络发送来解决。许多浏览器支持摘要式身份验证

HTTP摘要认证的标准由RFC 2617定义,该标准更新了RFC 2069规定的摘要认证的早期版本。大多数用户代理实现RFC2617。SpringSecurity的摘要认证支持与保护的“ auth”质量兼容( qop)是由RFC 2617规定的,它还提供了与RFC 2069的向后兼容性。如果您需要使用未加密的HTTP(即没有TLS / HTTPS)并且希望最大程度地提高身份验证过程的安全性,则摘要身份验证被视为一种更具吸引力的选择。但是,每个人都应该使用HTTPS

摘要式身份验证的核心是“一次性”。这是服务器生成的值。 Spring Security的现时采用以下格式:

例子60.摘要语法

1
2
3
base64(expirationTime + ":" + md5Hex(expirationTime + ":" + key))
expirationTime: The date and time when the nonce expires, expressed in milliseconds
key: A private key to prevent modification of the nonce token

您将需要确保使用NoOpPasswordEncoder配置不安全的纯文本密码存储。 以下提供了使用Java配置配置摘要式身份验证的示例:

例子61.摘要式认证

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
@Autowired
UserDetailsService userDetailsService;

DigestAuthenticationEntryPoint entryPoint() {
DigestAuthenticationEntryPoint result = new DigestAuthenticationEntryPoint();
result.setRealmName("My App Relam");
result.setKey("3028472b-da34-4501-bfd8-a355c42bdf92");
}

DigestAuthenticationFilter digestAuthenticationFilter() {
DigestAuthenticationFilter result = new DigestAuthenticationFilter();
result.setUserDetailsService(userDetailsService);
result.setAuthenticationEntryPoint(entryPoint());
}

protected void configure(HttpSecurity http) throws Exception {
http
// ...
.exceptionHandling(e -> e.authenticationEntryPoint(authenticationEntryPoint()))
.addFilterBefore(digestFilter());
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<b:bean id="digestFilter"
class="org.springframework.security.web.authentication.www.DigestAuthenticationFilter"
p:userDetailsService-ref="jdbcDaoImpl"
p:authenticationEntryPoint-ref="digestEntryPoint"
/>

<b:bean id="digestEntryPoint"
class="org.springframework.security.web.authentication.www.DigestAuthenticationEntryPoint"
p:realmName="My App Realm"
p:key="3028472b-da34-4501-bfd8-a355c42bdf92"
/>

<http>
<!-- ... -->
<custom-filter ref="userFilter" position="DIGEST_AUTH_FILTER"/>
</http>

内存中身份验证

Spring Security的InMemoryUserDetailsManager实现了UserDetailsService,以支持对在内存中检索到的基于用户名/密码的身份验证。 InMemoryUserDetailsManager通过实现UserDetailsManager接口来提供对UserDetails的管理。 当配置为接受用户名/密码进行身份验证时,Spring Security使用基于UserDetails的身份验证。

在此示例中,我们使用Spring Boot CLIpassword的密码进行编码,并获得{bcrypt}$2a$10$GRLdNijSQMUvl/au9ofL.eDwmoohzzS7.rmNSJZ.0FxO/BTk76klW的编码密码。

例子62. InMemoryUserDetailsManager Java配置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@Bean
public UserDetailsService users() {
UserDetails user = User.builder()
.username("user")
.password("{bcrypt}$2a$10$GRLdNijSQMUvl/au9ofL.eDwmoohzzS7.rmNSJZ.0FxO/BTk76klW")
.roles("USER")
.build();
UserDetails admin = User.builder()
.username("admin")
.password("{bcrypt}$2a$10$GRLdNijSQMUvl/au9ofL.eDwmoohzzS7.rmNSJZ.0FxO/BTk76klW")
.roles("USER", "ADMIN")
.build();
return new InMemoryUserDetailsManager(user, admin);
}
1
2
3
4
5
6
7
8
<user-service>
<user name="user"
password="{bcrypt}$2a$10$GRLdNijSQMUvl/au9ofL.eDwmoohzzS7.rmNSJZ.0FxO/BTk76klW"
authorities="ROLE_USER" />
<user name="admin"
password="{bcrypt}$2a$10$GRLdNijSQMUvl/au9ofL.eDwmoohzzS7.rmNSJZ.0FxO/BTk76klW"
authorities="ROLE_USER,ROLE_ADMIN" />
</user-service>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
@Bean
fun users(): UserDetailsService {
val user = User.builder()
.username("user")
.password("{bcrypt}$2a$10\$GRLdNijSQMUvl/au9ofL.eDwmoohzzS7.rmNSJZ.0FxO/BTk76klW")
.roles("USER")
.build()
val admin = User.builder()
.username("admin")
.password("{bcrypt}$2a$10\$GRLdNijSQMUvl/au9ofL.eDwmoohzzS7.rmNSJZ.0FxO/BTk76klW")
.roles("USER", "ADMIN")
.build()
return InMemoryUserDetailsManager(user, admin)
}

上面的示例以安全的格式存储密码,但是在入门经验方面还有很多不足之处。

在下面的示例中,我们利用User.withDefaultPasswordEncoder来确保存储在内存中的密码受到保护。 但是,它不能防止通过反编译源代码来获取密码。 因此,User.withDefaultPasswordEncoder仅应用于“入门”,而不应用于生产。

例子63.具有User.withDefaultPasswordEncoder的InMemoryUserDetailsManager

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@Bean
public UserDetailsService users() {
// The builder will ensure the passwords are encoded before saving in memory
UserBuilder users = User.withDefaultPasswordEncoder();
UserDetails user = users
.username("user")
.password("password")
.roles("USER")
.build();
UserDetails admin = users
.username("admin")
.password("password")
.roles("USER", "ADMIN")
.build();
return new InMemoryUserDetailsManager(user, admin);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@Bean
fun users(): UserDetailsService {
// The builder will ensure the passwords are encoded before saving in memory
val users = User.withDefaultPasswordEncoder()
val user = users
.username("user")
.password("password")
.roles("USER")
.build()
val admin = users
.username("admin")
.password("password")
.roles("USER", "ADMIN")
.build()
return InMemoryUserDetailsManager(user, admin)
}

没有简单的方法可以将User.withDefaultPasswordEncoder与基于XML的配置一起使用。 对于演示或刚刚入门,您可以选择在密码前加上{noop}前缀,以指示不应使用任何编码

例子64. {noop} XML配置

1
2
3
4
5
6
7
8
<user-service>
<user name="user"
password="{noop}password"
authorities="ROLE_USER" />
<user name="admin"
password="{noop}password"
authorities="ROLE_USER,ROLE_ADMIN" />
</user-service>

JDBC验证

UserDetails

UserDetailsService

PasswordEncoder

DaoAuthenticationProvider

LDAP Authentication

Session管理

Remember-Me Authentication

OpenID支持

匿名身份验证

验证前方案(Pre-Authentication Scenarios)

Java身份验证和授权服务(JAAS)提供程序

CAS Authentication

X.509 Authentication

运行身份验证替换

处理登出

身份验证事件

授权

Spring Security中的高级授权功能代表了其受欢迎程度的最令人信服的原因之一。 无论选择哪种身份验证(使用Spring Security提供的机制和提供程序,还是与容器或其他非Spring Security身份验证机构集成),您都会发现可以在您的应用程序中一致且简单地使用授权服务方式。

在这一部分中,我们将探索在第一部分中介绍的不同的AbstractSecurityInterceptor实现。然后,我们将继续探讨如何通过使用域访问控制列表来微调授权。

Authorization架构

使用FilterSecurityInterceptor授权HttpServletRequest

基于表达式的访问控制

Secure Object Implementations

Method Security

Domain Object Security (ACLs)

OAuth2

OAuth 2.0登录

OAuth 2.0登录功能为应用程序提供了让用户通过使用其在OAuth 2.0提供程序(例如GitHub)或OpenID Connect 1.0提供程序(例如Google)上的现有帐户登录该应用程序的功能。 OAuth 2.0登录实现了以下用例:“使用Google登录”或“使用GitHub登录”。

SAML2

SAML 2.0登录

SAML 2.0登录名saml2Login()功能为应用程序提供了使用户能够通过使用其在SAML 2.0身份提供程序(Okta,ADFS等)上的现有帐户登录到该应用程序的功能。

防止利用漏洞

Servlet环境的跨站点请求伪造(CSRF)

本部分讨论了Spring Security对Servlet环境的跨站点请求伪造(CSRF)支持。

整合方式

Servlet API集成

本节描述了如何将Spring Security与Servlet API集成在一起。 servletapi-xml样本应用程序演示了每种方法的用法。

Java配置

在Spring 3.1中,对Java配置的常规支持已添加到Spring Framework中。 从Spring Security 3.2开始,Spring Security Java Configuration支持使用户可以轻松配置Spring Security,而无需使用任何XML。

如果您熟悉安全命名空间配置,则应该在它与安全Java配置支持之间找到很多相似之处。

Kotlin配置

从Spring Security 5.3开始,Spring Security Kotlin Configuration支持已经可用。 它使用户可以使用本地Kotlin DSL轻松配置Spring Security。

测试

本节描述了Spring Security提供的测试支持。

Tip

要使用Spring Security测试支持,必须包含spring-security-test-5.3.4.RELEASE.jar作为项目的依赖项。

Spring Security加密模块

介绍

Spring Security Crypto模块提供对对称加密,密钥生成和密码编码的支持。 该代码作为核心模块的一部分分发,但与任何其他Spring Security(或Spring)代码无关。

附录

安全数据库模式

框架使用了各种数据库模式,本附录为它们提供了单个参考点。 您只需要提供所需功能范围的表即可。

为HSQLDB数据库提供了DDL语句。 您可以将这些用作定义正在使用的数据库的架构的准则。

Reactive Applications

WebFlux安全

Spring Security的WebFlux支持依赖于WebFilter,并且对Spring WebFlux和Spring WebFlux.Fn相同。 您可以找到一些示例程序来演示以下代码:

防止利用漏洞

WebFlux环境的跨站点请求伪造(CSRF)

本节讨论Spring Security对WebFlux环境的跨站点请求伪造(CSRF)支持。

OAuth2 WebFlux

Spring Security为响应式应用程序提供了OAuth2和WebFlux集成。

@RegisteredOAuth2AuthorizedClient

Spring Security允许使用@ RegisteredOAuth2AuthorizedClient解析访问令牌。

响应式X.509身份验证

与Servlet X.509身份验证类似,反应性x509身份验证过滤器允许从客户端提供的证书中提取身份验证令牌。

WebClient

以下文档供在Reactive环境中使用。 对于Servlet环境,请参阅Servlet环境的WebClient。

EnableReactiveMethodSecurity

Spring Security使用Reactor的Context支持方法安全,该上下文是使用ReactiveSecurityContextHolder设置的。 例如,这演示了如何检索当前登录用户的消息。

Reactive测试支持

测试Reactive方法的安全性

例如,我们可以使用与测试方法安全性相同的设置和注释,从EnableReactiveMethodSecurity中测试示例。 这是我们可以做的最小样本:

RSocket安全

Spring Security的RSocket支持依赖于SocketAcceptorInterceptor。 在PayloadSocketAcceptorInterceptor中可以找到安全性的主要入口点,该接口使RSocket API适应于允许通过PayloadInterceptor实现拦截PayloadExchange。