Spring Security
Spring Security
Jie一. 概述
Spring Security 是一个 Java 框架,用于保护应用程序的安全性。它提供了一套全面的安全解决方案,包括身份验证、授权、防止攻击等功能。Spring Security 基于过滤器链的概念,可以轻松地集成到任何基于 Spring 的应用程序中。它支持多种身份验证选项和授权策略,开发人员可以根据需要选择适合的方式。此外,Spring Security 还提供了一些附加功能,如集成第三方身份验证提供商和单点登录,以及会话管理和密码编码等。总之,Spring Security 是一个强大且易于使用的框架,可以帮助开发人员提高应用程序的安全性和可靠性。
二. 先决条件
Spring Security 要求有 Java 8 或更高的运行环境。
由于 Spring Security 旨在以独立的方式运行,你不需要在你的 Java 运行时环境中放置任何特殊的配置文件。特别是,你不需要配置一个特殊的 Java 认证和授权服务(JAAS)策略文件,也不需要把 Spring Security 放到普通的 classpath 位置。
同样地,如果你使用 EJB 容器或 Servlet 容器,你不需要把任何特殊的配置文件放在任何地方,也不需要把 Spring Security 包含在服务器的 classloader 中。所有需要的文件都包含在你的应用程序中。
这种设计提供了最大的部署时间灵活性,因为你可以将你的目标工件(无论是 JAR、WAR 还是 EAR)从一个系统复制到另一个系统,并立即运行。
三. 源码
你可以在 GitHub 上找到 Spring Security 的源代码:https://github.com/spring-projects/spring-security/
四. 获取 Spring Security
版本号
Spring Security 版本的格式为 MAJOR.MINOR.PATCH。
MAJOR 版本可能包含破坏性变化。通常情况下,这些改动是为了提供更好的安全性,以符合现代安全实践。
MINOR 版本包含增强功能,但被认为是被动更新。
PATCH 级别应该是完全兼容的,向前和向后,可能的例外是修复错误的变化。
在 Maven 中使用
与大多数开源项目一样,Spring Security 以 Maven 工件的形式部署其依赖项。本节中的主题描述了在使用 Maven 时如何使用 Spring Security。
Spring Boot 和 Maven
Spring Boot 提供了一个 spring-boot-starter-security starter,聚合了 Spring Security 相关的依赖。使用 starter 的最简单和首选方式是通过使用 IDE 集成( Eclipse 或 IntelliJ、 NetBeans)或通过 start.spring.io 使用 Spring Initializr。另外,你也可以手动添加 starter,如下面的例子所示。
1 | <dependencies> |
由于 Spring Boot 提供了一个 Maven BOM 来管理依赖版本,所以你不需要指定一个版本。如果你想覆盖 Spring Security 的版本,你可以通过提供一个 Maven 属性来实现。
1 | <properties> |
由于 Spring Security 只在主要(MAJOR)的版本中进行突破性的修改,所以你可以安全地在 Spring Boot 中使用较新版本的 Spring Security。不过,有时你可能也需要更新 Spring Framework 的版本。您可以通过添加一个 Maven 属性来做到这一点。
1 | <properties> |
五. 特性
Section Summary
1.认证(Authentication)
Spring Security 提供了对 认证(authentication) 的全面支持。认证是指我们如何验证试图访问特定资源的人的身份。一个常见的验证用户的方法是要求用户输入用户名和密码。一旦进行了认证,我们就知道了身份并可以执行授权。
Spring Security 提供了对用户认证的内置支持。本节专门介绍通用的认证支持,适用于 Servlet 和 WebFlux 环境。请参阅 Servlet 和 WebFlux 的认证部分,了解每个技术栈所支持的细节。
密码存储
Spring Security
的 PasswordEncoder
接口用于对密码进行单向转换,让密码安全地存储。鉴于 PasswordEncoder
是一个单向转换,当密码转换需要双向时(如存储用于验证数据库的凭证),它就没有用了。通常情况下,PasswordEncoder 用于存储在认证时需要与用户提供的密码进行比较的密码。
DelegatingPasswordEncoder
在 Spring Security 5.0 之前,默认的 PasswordEncoder
是 NoOpPasswordEncoder
,它需要纯文本密码。根据密码历史部分,你可能期望现在默认的 PasswordEncoder 是类似 BCryptPasswordEncoder 的东西。然而,这忽略了三个现实世界的问题。
- 许多应用程序使用旧的密码编码(password encode),不能轻易迁移。
- 密码存储的最佳实践将再次改变。
- 作为一个框架,Spring Security 不能频繁地进行破坏性的改变。
相反,Spring Security 引入了 DelegatingPasswordEncoder,它通过以下方式解决了所有的问题。
- 确保通过使用当前的密码存储建议对密码进行编码。
- 允许验证现代和传统格式的密码。
- 允许在未来升级编码。
你可以通过使用 PasswordEncoderFactories 轻松构建 DelegatingPasswordEncoder 的实例。
Create Default DelegatingPasswordEncoder
1 | PasswordEncoder passwordEncoder = |
另外,你也可以创建自己的自定义实例。
Create Custom DelegatingPasswordEncoder
1 | String idForEncode = "bcrypt"; |
密码存储格式
密码的一般格式是:
DelegatingPasswordEncoder Storage Format
1 | {id}encodedPassword |
id
是一个标识符,用于查询应该使用哪个 PasswordEncoder
,encodedPassword
是所选 PasswordEncoder
的原始编码密码。id 必须在密码的开头,以 {
开始,以 }
结束。如果找不到 id
,id
将被设置为 null。例如,下面可能是一个使用不同 id 值编码的密码列表。所有的原始密码都是 password。
DelegatingPasswordEncoder Encoded Passwords Example
1 | {bcrypt}$2a$10$dXJ3SW6G7P50lGmMkkmwe.20cQQubK3.HZWzG3YB1tlRy.fqvM/BG |
第一个密码的 PasswordEncoder id为 bcrypt,encodedPassword 值为$2a$10$dXJ3SW6G7P50lGmMkkmwe.20cQQubK3.HZWzG3YB1tlRy.fqvM/BG。匹配时,它将委托给 BCryptPasswordEncoder
第二个密码的 PasswordEncoder id为 noop,encodedPassword 值为 password。匹配时,它将委托给 NoOpPasswordEncoder。
第三个密码的 PasswordEncoder id为 pbkdf2,encodedPassword 值为 5d923b44a6d129f3ddf3e3c8d29412723dcbde72445e8ef6bf3b508fbf17fa4ed4d6b99ca763d8dc。匹配时,它将委托给 Pbkdf2PasswordEncoder。
第四个密码的 PasswordEncoder id为 scrypt,encodedPassword 值为 $e0801$8bWJaSu2IKSn9Z9kM+TPXfOc/9bdYSrN1oD9qfVThWEwdRTnO7re7Ei+fUZRJ68k9lTyuTeUp4of4g24hHnazw==$OAOec05+bXxvuu/1qZ6NUR+xQYvYv7BeL1QxwRpY5Pc= 。匹配时,它将委托给 SCryptPasswordEncoder。
最后一个密码的 PasswordEncoder id为 sha256,encodedPassword 值为 97cde38028ad898ebc02e690819fa220e88c62e0699403e94fff291cfffaf8410849f27605abcbc0。匹配时,它将委托给 StandardPasswordEncoder。
_一些用户可能会担心,存储格式是为潜在的黑客提供的。这不是一个问题,因为密码的存储并不依赖于算法是一个秘密。此外,大多数格式在没有前缀的情况下,攻击者很容易搞清楚。例如,BCrypt 密码经常以 $2a$
开始。_\
密码编码
传递给构造函数的 idForEncode
决定了哪一个 PasswordEncoder
被用于编码密码。在我们之前构建的 DelegatingPasswordEncoder
中,这意味着编码密码的结果被委托给 BCryptPasswordEncoder
,并以 {bcrypt}
为前缀。最终的结果看起来像下面的例子。
DelegatingPasswordEncoder Encode Example
1 | {bcrypt}$2a$10$dXJ3SW6G7P50lGmMkkmwe.20cQQubK3.HZWzG3YB1tlRy.fqvM/BG |
密码匹配(对比)
匹配是基于 {id}
和构造函数中提供的 id
到 PasswordEncoder
的映射。我们在密码存储格式中的例子提供了一个如何实现的工作实例。默认情况下,用一个密码和一个没有映射的 id(包括空 id)调用 matches(CharSequence, String)
的结果是 IllegalArgumentException
。这个行为可以通过使用 DelegatingPasswordEncoder.setDefaultPasswordEncoderForMatches(PasswordEncoder)
来定制。
通过使用 id
,我们可以在任何密码编码上进行匹配,但通过使用最现代的密码编码对密码进行编码。这一点很重要,因为与加密不同,密码散列(Hash)的设计使我们没有简单的方法来恢复明文。既然没有办法恢复明文,那么就很难迁移密码了。虽然用户迁移 NoOpPasswordEncoder
很简单,但我们选择默认包含它,以使它的入门体验更简单。
入门体验
如果你正在制作一个演示或样本,花时间对用户的密码进行哈希处理是有点麻烦的。有一些方便的机制可以使之更容易,但这仍然不是为生产准备的。
withDefaultPasswordEncoder Example
1 | UserDetails user = User.withDefaultPasswordEncoder() |
如果你要创建多个用户,你也可以重复使用 builder。
withDefaultPasswordEncoder Reusing the Builder
1 | UserBuilder users = User.withDefaultPasswordEncoder(); |
这确实对存储的密码进行了哈希处理,但密码仍然暴露在内存和编译后的源代码中。因此,对于生产环境来说,它仍然不被认为是安全的。对于生产来说,你应该在外部对你的密码进行散列(Hash)。
用 Spring Boot CLI 进行编码
对密码进行正确编码的最简单方法是使用 Spring Boot CLI
。
例如,下面的例子对 password
的密码进行编码,以便与 DelegatingPasswordEncoder
一起使用。
Spring Boot CLI encodepassword Example
1 | spring encodepassword password |
故障排除
如密码存储格式中所述,当被存储的密码之一没有 id 时,会出现以下错误。
1 | java.lang.IllegalArgumentException: There is no PasswordEncoder mapped for the id "null" |
解决这个问题的最简单方法是弄清楚你的密码目前是如何存储的,并明确地提供正确的 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
的默认实现使用 BCryptPasswordEncoder
的 Javadoc
中提到的强度 10。我们鼓励你在自己的系统上调整和测试强度参数,使其大约需要 1 秒钟来验证一个密码。
BCryptPasswordEncoder
1 | // Create an encoder with strength 16 |
Argon2PasswordEncoder
Argon2PasswordEncoder
的实现使用 Argon2
算法对密码进行散列。Argon2
是 密码哈希大赛
的冠军。为了打败定制硬件上的密码破解,Argon2
是一种故意的慢速算法,需要大量的内存。像其他自适应单向函数一样,它应该被调整为在你的系统上验证一个密码需要 1 秒左右。 Argon2PasswordEncoder
的当前实现需要 BouncyCastle
。
Argon2PasswordEncoder
1 | // Create an encoder with all the defaults |
Pbkdf2PasswordEncoder
Pbkdf2PasswordEncoder
的实现使用 PBKDF2
算法对密码进行散列。为了抵御密码破解,PBKDF2 是一种故意的慢速算法。像其他自适应单向函数一样,它应该被调整为在你的系统上验证一个密码需要 1 秒左右。当需要 FIPS 认证时,这种算法是一个不错的选择。
Pbkdf2PasswordEncoder
1 | // Create an encoder with all the defaults |
SCryptPasswordEncoder
SCryptPasswordEncoder
的实现使用 scrypt
算法对密码进行散列。为了打败定制硬件上的密码破解,scrypt 是一个故意的慢速算法,需要大量的内存。像其他自适应单向函数一样,它应该被调整为在你的系统上验证一个密码需要 1 秒左右。
SCryptPasswordEncoder
1 | // Create an encoder with all the defaults |
其他 PasswordEncoder
有相当数量的其他 PasswordEncoder 实现,它们的存在完全是为了向后兼容。它们都被废弃了,表明它们不再被认为是安全的。然而,没有计划删除它们,因为迁移现有的遗留系统很困难。
密码存储配置
Spring Security
默认使用 DelegatingPasswordEncoder
。然而,你可以通过将 PasswordEncoder
暴露为 Spring Bean
来进行定制。
如果你是从 Spring Security 4.2.x
迁移过来的,你可以通过暴露一个 NoOpPasswordEncoder Bean
来恢复到以前的行为。
恢复到 NoOpPasswordEncoder
被认为是 不安全
的。你应该转而使用 DelegatingPasswordEncoder
来支持安全的密码编码。
NoOpPasswordEncoder
1 |
|
1 | <b:bean id="passwordEncoder" |
在 XML 配置下,要求 NoOpPasswordEncoder Bean
的名称为 passwordEncoder
。
更改密码配置
大多数允许用户指定密码的应用程序也需要一个更新密码的功能。
用于更改密码的 Well-Known URL
表示一种机制,密码管理器可以通过该机制发现特定应用程序的密码更新端点。
你可以配置 Spring Security
来提供这个发现端点。例如,如果你的应用程序中更改密码的端点是 /change-password
,那么你可以这样配置 Spring Security
。
Default Change Password Endpoint
1 | http |
1 | <sec:password-management/> |
然后,当密码管理器导航到 /.well-known/change-password
时,Spring Security
将重定向你的端点,/change-password
。
或者,如果你的端点是 /change-password
以外的东西,你也可以像这样指定。
Change Password Endpoint
1 | http |
1 | <sec:password-management change-password-page="/update-password"/> |
通过上述配置,当密码管理器导航到 /.well-known/change-password
时,那么 Spring Security
将重定向到 /update-password
。
2.防范漏洞攻击
Spring Security 提供对常见漏洞的保护。只要有可能,这种保护就会以默认方式启用。本节描述了 Spring Security
所保护的各种漏洞。
Section Summary
CSRF
Spring 提供了对 跨站请求伪造(CSRF) 攻击的全面支持。在下面的章节中,我们将探讨。
- 什么是 CSRF 攻击?
- 防范 CSRF 攻击
- CSRF 的考虑因素
HTTP Header
你可以通过多种方式使用 HTTP响应头
来提高 Web 应用程序的安全性。本节将专门介绍 Spring Security
提供明确支持的各种 HTTP 响应头。如果有必要,你也可以配置 Spring Security
来提供 自定义 header
信息。
HTTP
所有基于 HTTP 的通信,包括 静态资源
,都应该通过使用 TLS
进行保护。
作为一个框架,Spring Security
并不处理 HTTP 连接,因此并不直接提供对 HTTPS 的支持。然而,它确实提供了一些有助于 HTTPS 使用的功能。
3.整合
Spring Security 提供了与众多框架和 API 的集成。在本节中,我们将讨论不针对 Servlet 或 Reactive 环境的通用集成。要查看具体的集成,请参考 Servlet 和 Reactive 集成部分。
Section Summary
- 密码学
Spring Security Crypto 模块提供对对称加密、密钥生成和密码编码的支持。该代码作为核心模块的一部分发布,但与任何其他 Spring Security(或 Spring)代码没有依赖关系。 - Spring Data
Spring Security 提供的 Spring Data 集成允许在你的查询中引用当前用户。在查询中包含用户,以支持分页结果,这不仅是有用的,而且是必要的,因为事后过滤结果将无法扩展。 - Java 的并发 API
在大多数环境中,Security 是以每个 Thread 为单位进行存储的。这意味着,当在一个新的线程上工作时,SecurityContext 就会丢失。Spring Security 提供了一些基础设施,以帮助用户更容易地处理这个问题。Spring Security 为在多线程环境中使用 Spring Security 提供了低层次的抽象。事实上,这正是 Spring Security 与 AsyncContext.start(Runnable) 和 Spring MVC Async 整合的基础。 - Jackson
Spring Security 为持久化 Spring Security 相关的类提供了 Jackson 支持。这可以提高在使用分布式会话(即 session replication、Spring Session 等)时序列化 Spring Security 相关类的性能。 - 本地化
Spring Security 支持对终端用户可能看到的异常信息进行本地化。如果你的应用程序是为讲英语的用户设计的,你不需要做任何事情,因为默认情况下所有的安全消息都是英文的。如果你需要支持其他地区,你需要知道的一切都包含在本节中。
所有的异常信息都可以被本地化,包括与认证失败和访问被拒绝(授权失败)有关的信息。针对开发者或系统部署者的异常和日志信息(包括不正确的属性、违反接口约定、使用不正确的构造函数、启动时验证、debug 级日志)不被本地化,就直接在 Spring Security 的代码中用英/中文硬编码。