Spring Security特性(密码)

Spring Security特性(密码)

Spring Security提供了对 认证(authentication) 的全面支持。认证是指我们如何验证试图访问特定资源的人的身份。一个常见的验证用户的方法是要求用户输入用户名和密码。一旦进行了认证,我们就知道了身份并可以执行授权。

Spring Security提供了对用户认证的内置支持。本节专门介绍通用的认证支持,适用于Servlet和WebFlux环境。请参阅 Servlet 和WebFlux的认证部分,了解每个技术栈所支持的细节。

密码存储

Spring SecurityPasswordEncoder 接口用于对密码进行单向转换,让密码安全地存储。鉴于 PasswordEncoder 是一个单向转换,当密码转换需要双向时(如存储用于验证数据库的凭证),它就没有用了。通常情况下,PasswordEncoder 用于存储在认证时需要与用户提供的密码进行比较的密码。

DelegatingPasswordEncoder

在Spring Security 5.0之前,默认的 PasswordEncoder NoOpPasswordEncoder,它需要纯文本密码。根据密码历史部分,你可能期望现在默认的 PasswordEncoder 是类似 BCryptPasswordEncoder 的东西。然而,这忽略了三个现实世界的问题。

  1. 许多应用程序使用旧的密码编码(password encode),不能轻易迁移。
  2. 密码存储的最佳实践将再次改变。
  3. 作为一个框架,Spring Security 不能频繁地进行破坏性的改变。

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

  1. 确保通过使用当前的密码存储建议对密码进行编码。
  2. 允许验证现代和传统格式的密码。
  3. 允许在未来升级编码。

你可以通过使用 PasswordEncoderFactories 轻松构建 DelegatingPasswordEncoder 的实例。
Create Default DelegatingPasswordEncoder

java 复制代码
PasswordEncoder passwordEncoder =
    PasswordEncoderFactories.createDelegatingPasswordEncoder();

另外,你也可以创建自己的自定义实例。
Create Custom DelegatingPasswordEncoder

java 复制代码
String idForEncode = "bcrypt";
Map encoders = new HashMap<>();
encoders.put(idForEncode, new BCryptPasswordEncoder());
encoders.put("noop", NoOpPasswordEncoder.getInstance());
encoders.put("pbkdf2", Pbkdf2PasswordEncoder.defaultsForSpringSecurity_v5_5());
encoders.put("pbkdf2@SpringSecurity_v5_8", Pbkdf2PasswordEncoder.defaultsForSpringSecurity_v5_8());
encoders.put("scrypt", SCryptPasswordEncoder.defaultsForSpringSecurity_v4_1());
encoders.put("scrypt@SpringSecurity_v5_8", SCryptPasswordEncoder.defaultsForSpringSecurity_v5_8());
encoders.put("argon2", Argon2PasswordEncoder.defaultsForSpringSecurity_v5_2());
encoders.put("argon2@SpringSecurity_v5_8", Argon2PasswordEncoder.defaultsForSpringSecurity_v5_8());
encoders.put("sha256", new StandardPasswordEncoder());

PasswordEncoder passwordEncoder =
    new DelegatingPasswordEncoder(idForEncode, encoders);
密码存储格式

密码的一般格式是:
DelegatingPasswordEncoder Storage Format

text 复制代码
{id}encodedPassword

id 是一个标识符,用于查询应该使用哪个 PasswordEncoderencodedPassword 是所选 PasswordEncoder 的原始编码密码。id 必须在密码的开头,以 { 开始,以 } 结束。如果找不到 idid 将被设置为null。例如,下面可能是一个使用不同 id 值编码的密码列表。所有的原始密码都是 password。
DelegatingPasswordEncoder Encoded Passwords Example

text 复制代码
{bcrypt}$2a$10$dXJ3SW6G7P50lGmMkkmwe.20cQQubK3.HZWzG3YB1tlRy.fqvM/BG
{noop}password
{pbkdf2}5d923b44a6d129f3ddf3e3c8d29412723dcbde72445e8ef6bf3b508fbf17fa4ed4d6b99ca763d8dc
{scrypt}$e0801$8bWJaSu2IKSn9Z9kM+TPXfOc/9bdYSrN1oD9qfVThWEwdRTnO7re7Ei+fUZRJ68k9lTyuTeUp4of4g24hHnazw==$OAOec05+bXxvuu/1qZ6NUR+xQYvYv7BeL1QxwRpY5Pc=
{sha256}97cde38028ad898ebc02e690819fa220e88c62e0699403e94fff291cfffaf8410849f27605abcbc0
第一个密码的 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

text 复制代码
{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

Java 复制代码
UserDetails user = User.withDefaultPasswordEncoder()
  .username("user")
  .password("password")
  .roles("user")
  .build();
System.out.println(user.getPassword());
// {bcrypt}$2a$10$dXJ3SW6G7P50lGmMkkmwe.20cQQubK3.HZWzG3YB1tlRy.fqvM/BG

如果你要创建多个用户,你也可以重复使用builder。

withDefaultPasswordEncoder Reusing the Builder

Java 复制代码
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();

这确实对存储的密码进行了哈希处理,但密码仍然暴露在内存和编译后的源代码中。因此,对于生产环境来说,它仍然不被认为是安全的。对于生产来说,你应该在外部对你的密码进行散列(Hash)。

用Spring Boot CLI进行编码

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

例如,下面的例子对 password 的密码进行编码,以便与 DelegatingPasswordEncoder 一起使用。

Spring Boot CLI encodepassword Example

text 复制代码
spring encodepassword password
{bcrypt}$2a$10$X5wFBtLrL/kHcmrOGGTrGufsBX8CJ0WpQpF3pgeuxBB/H73BK1DW6
故障排除

如密码存储格式中所述,当被存储的密码之一没有 id 时,会出现以下错误。

bash 复制代码
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。

如果你是从Spring Security 4.2.x迁移过来的,你可以通过暴露一个 NoOpPasswordEncoder bean来恢复到以前的行为。

另外,你可以在所有的密码前加上正确的 id,并继续使用 DelegatingPasswordEncoder。例如,如果你使用的是BCrypt,你可以将你的密码从类似的地方迁移过来。

text 复制代码
$2a$10$dXJ3SW6G7P50lGmMkkmwe.20cQQubK3.HZWzG3YB1tlRy.fqvM/BG

迁移为如下:

text 复制代码
{bcrypt}$2a$10$dXJ3SW6G7P50lGmMkkmwe.20cQQubK3.HZWzG3YB1tlRy.fqvM/BG

关于映射的完整列表,请参见 PasswordEncoderFactories 的Javadoc。

BCryptPasswordEncoder

BCryptPasswordEncoder 的实现使用广泛支持的 bcrypt 算法对密码进行散列。为了使它对密码破解有更强的抵抗力,bcrypt故意做得很慢。像其他自适应单向函数一样,它应该被调整为在你的系统上验证一个密码需要1秒左右。BCryptPasswordEncoder 的默认实现使用 BCryptPasswordEncoder Javadoc 中提到的强度10。我们鼓励你在自己的系统上调整和测试强度参数,使其大约需要1秒钟来验证一个密码。

BCryptPasswordEncoder

Java 复制代码
// Create an encoder with strength 16
BCryptPasswordEncoder encoder = new BCryptPasswordEncoder(16);
String result = encoder.encode("myPassword");
assertTrue(encoder.matches("myPassword", result));
Argon2PasswordEncoder

Argon2PasswordEncoder 的实现使用 Argon2 算法对密码进行散列。Argon2密码哈希大赛 的冠军。为了打败定制硬件上的密码破解,Argon2是一种故意的慢速算法,需要大量的内存。像其他自适应单向函数一样,它应该被调整为在你的系统上验证一个密码需要1秒左右。 Argon2PasswordEncoder 的当前实现需要 BouncyCastle

Argon2PasswordEncoder

Java 复制代码
// Create an encoder with all the defaults
Argon2PasswordEncoder encoder = Argon2PasswordEncoder.defaultsForSpringSecurity_v5_8();
String result = encoder.encode("myPassword");
assertTrue(encoder.matches("myPassword", result));
Pbkdf2PasswordEncoder

Pbkdf2PasswordEncoder 的实现使用 PBKDF2 算法对密码进行散列。为了抵御密码破解,PBKDF2是一种故意的慢速算法。像其他自适应单向函数一样,它应该被调整为在你的系统上验证一个密码需要1秒左右。当需要FIPS认证时,这种算法是一个不错的选择。

Pbkdf2PasswordEncoder

Java 复制代码
// Create an encoder with all the defaults
Pbkdf2PasswordEncoder encoder = Pbkdf2PasswordEncoder.defaultsForSpringSecurity_v5_8();
String result = encoder.encode("myPassword");
assertTrue(encoder.matches("myPassword", result));
SCryptPasswordEncoder

SCryptPasswordEncoder的实现使用 scrypt 算法对密码进行散列。为了打败定制硬件上的密码破解,scrypt是一个故意的慢速算法,需要大量的内存。像其他自适应单向函数一样,它应该被调整为在你的系统上验证一个密码需要1秒左右。

SCryptPasswordEncoder

Java 复制代码
// Create an encoder with all the defaults
SCryptPasswordEncoder encoder = SCryptPasswordEncoder.defaultsForSpringSecurity_v5_8();
String result = encoder.encode("myPassword");
assertTrue(encoder.matches("myPassword", result));
其他 PasswordEncoder

有相当数量的其他 PasswordEncoder 实现,它们的存在完全是为了向后兼容。它们都被废弃了,表明它们不再被认为是安全的。然而,没有计划删除它们,因为迁移现有的遗留系统很困难。

密码存储配置

Spring Security 默认使用 DelegatingPasswordEncoder。然而,你可以通过将 PasswordEncoder 暴露为 Spring Bean 来进行定制。

如果你是从 Spring Security 4.2.x 迁移过来的,你可以通过暴露一个 NoOpPasswordEncoder Bean 来恢复到以前的行为。

恢复到 NoOpPasswordEncoder 被认为是 不安全的。你应该转而使用 DelegatingPasswordEncoder来支持安全的密码编码。

NoOpPasswordEncoder

Java 复制代码
@Bean
public static NoOpPasswordEncoder passwordEncoder() {
    return NoOpPasswordEncoder.getInstance();
}
xml 复制代码
<b:bean id="passwordEncoder"
    class="org.springframework.security.crypto.password.NoOpPasswordEncoder" factory-method="getInstance"/>

在XML 配置下,要求 NoOpPasswordEncoder Bean的名称为 passwordEncoder

更改密码配置

大多数允许用户指定密码的应用程序也需要一个更新密码的功能。

用于更改密码的 Well-Known URL 表示一种机制,密码管理器可以通过该机制发现特定应用程序的密码更新端点。

你可以配置 Spring Security 来提供这个发现端点。例如,如果你的应用程序中更改密码的端点是 /change-password,那么你可以这样配置 Spring Security

Default Change Password Endpoint

Java 复制代码
http
    .passwordManagement(Customizer.withDefaults())
xml 复制代码
<sec:password-management/>

然后,当密码管理器导航到 /.well-known/change-password 时,Spring Security 将重定向你的端点,/change-password

或者,如果你的端点是 /change-password 以外的东西,你也可以像这样指定。

Change Password Endpoint

Java 复制代码
http
    .passwordManagement((management) -> management
        .changePasswordPage("/update-password")
    )
xml 复制代码
<sec:password-management change-password-page="/update-password"/>

通过上述配置,当密码管理器导航到 /.well-known/change-password 时,那么 Spring Security 将重定向到 /update-password

相关推荐
web135085886353 分钟前
前端node.js
前端·node.js·vim
m0_512744644 分钟前
极客大挑战2024-web-wp(详细)
android·前端
栗豆包9 分钟前
w118共享汽车管理系统
java·spring boot·后端·spring·tomcat·maven
夜半被帅醒15 分钟前
MySQL 数据库优化详解【Java数据库调优】
java·数据库·mysql
万亿少女的梦16821 分钟前
基于Spring Boot的网络购物商城的设计与实现
java·spring boot·后端
潜意识起点28 分钟前
精通 CSS 阴影效果:从基础到高级应用
前端·css
不爱学习的啊Biao29 分钟前
【13】MySQL如何选择合适的索引?
android·数据库·mysql
奋斗吧程序媛32 分钟前
删除VSCode上 origin/分支名,但GitLab上实际上不存在的分支
前端·vscode
破 风39 分钟前
SpringBoot 集成 MongoDB
数据库·mongodb
汤姆和佩琦39 分钟前
2024-12-25-sklearn学习(20)无监督学习-双聚类 料峭春风吹酒醒,微冷,山头斜照却相迎。
学习·聚类·sklearn