相关文章:

  1. OAuth2的定义和运行流程
  2. Spring Security OAuth实现Gitee快捷登录
  3. Spring Security OAuth实现GitHub快捷登录
  4. Spring Security的过滤器链机制
  5. Spring Security OAuth Client配置加载源码分析
  6. Spring Security内置过滤器详解
  7. 为什么加载了两个OAuth2AuthorizationRequestRedirectFilter分析
  8. Spring Security 自定义授权服务器实践
  9. Spring Security 自定义资源服务器实践
  10. Spring Security 自定义用户信息端点与多种登录方式共存

Spring Security的PasswordEncoder接口用于执行密码的单向转换,以便可以安全的存储密码。PasswordEncoder通常用于在认证时将用户提供的密码与存储的密码的比较。

密码存储的历史

多年来存储密码的标准机制不断发展,起初以明文的形式存储。大量恶意用户通过恶意注入等方式获取大量的用户名和密码。随着越来越多的用户认证成为现实,公共安全专家意识到我们需要做更多的工作来保护用户的密码。
建议开发人员在通过单向散列(如SHA-256)加密密码后存储密码。当用户尝试进行身份验证时,哈希密码将与他们键入的密码的哈希值进行比较,因此,系统只需要存储密码的单向散列值,如果发生泄露,也只会暴露密码的单向散列值。由于散列是一种单向形式,在给定散列的情况下很难猜测出密码,因此不值得费尽心思找出系统中的每个密码。但是恶意用户创建了彩虹表(Rainbow Tables),他们不是每次都猜测密码,而是计算一次密码并将其存储在查找表中。

为了降低彩虹表的有效性,建议开发者使用加盐(salt)的密码,盐(salt)为每个用户的密码生成一个随机数,将salt和用户密码通过哈希函数计算,得到唯一的哈希值。salt将以明文形式存储在用户密码中?,当用户认证的时候,存储的哈希值跟salt和用户密码的哈希值进行比较。

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

因此现在建议开发者使用自适应单向函数存储密码,验证一个自适应单向函数密码会占用一定的资源(比如CPU、内存等)。自适应单向功能允许配置“工作因子”,随着硬件的改进,工作因子会增加,建议将“工作因子”调整为大约1秒,以验证系统上的密码。这种权衡使得攻击者很难破解密码,并且成本不会太高,不会给您自己的系统带来太大的负担。Spring Security 提供了“工作因子”,但还是建议用户自定义自己的“工作因子”,因为不同系统的性能差异大。可以使用的自适应单向函数,包括 bcrypt, PBKDF2, scrypt, 和argon2

由于自适应单向函数会占用大量资源,因此在验证用户名和密码时将显著降低应用程序的性能。Spring Security(或任何其他库)都无法加速密码验证,因为安全性是通过使用资源密集计算来获得的。建议用户将长期凭证(即用户名和密码)替换为短期凭证(即会话、OAuth令牌等)。可以快速验证短期凭证,而不会丢失任何安全性。

DelegatingPasswordEncoder

在Spring Security 5.0之前,默认密码编码器是NoOpPasswordEncoder,它需要纯文本密码。根据上面的密码历史部分,您可能会认为默认密码编码器现在类似于BCryptPasswordEncoder。然而,这忽略了三个现实问题:

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

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

  • 使用当前建议的方式确保密码被正确编码
  • 允许使用现代和传统格式验证密码
  • 允许将来升级编码

使用PasswordEncoderFactories能简单的构造出DelegatingPasswordEncoderExample 1. Create Default DelegatingPasswordEncoder

PasswordEncoder passwordEncoder =
    PasswordEncoderFactories.createDelegatingPasswordEncoder();

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

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);

密码存储格式
密码的一般个是为:
Example 3. DelegatingPasswordEncoder Storage Format

{id}encodedPassword

id表示使用哪种PasswordEncoder,必须位于密码的开头,以{开始,以}结束,如id未找到,则为null
encodedPassword表示需要编码的原始密码
如下是不同id的密码列表:

①{bcrypt}$2a$10$dXJ3SW6G7P50lGmMkkmwe.20cQQubK3.HZWzG3YB1tlRy.fqvM/BG
②{noop}password
③{pbkdf2}5d923b44a6d129f3ddf3e3c8d29412723dcbde72445e8ef6bf3b508fbf17fa4ed4d6b99ca763d8dc
④{scrypt}$e0801$8bWJaSu2IKSn9Z9kM+TPXfOc/9bdYSrN1oD9qfVThWEwdRTnO7re7Ei+fUZRJ68k9lTyuTeUp4of4g24hHnazw==$OAOec05+bXxvuu/1qZ6NUR+xQYvYv7BeL1QxwRpY5Pc=
⑤{sha256}97cde38028ad898ebc02e690819fa220e88c62e0699403e94fff291cfffaf8410849f27605abcbc0

①bcrypt的PasswordEncoder id,编码密码是:$2aspringclound oauth2 密码模式 设置过期时间_springdXJ3SW6G7P50lGmMkkmwe.20cQQubK3.HZWzG3YB1tlRy.fqvM/BG,匹配时委托给BCryptPasswordEncoder
②noop的PasswordEncoder,编码密码是:password,匹配委托给NoOpPasswordEncoder
③pbkdf2的PasswordEncoder,编码密码是:5d923b44a6d129f3ddf3e3c8d29412723dcbde72445e8ef6bf3b508fbf17fa4ed4d6b99ca763d8dc,匹配委托给Pbkdf2PasswordEncoder
④scrypt的PasswordEncoder,编码密码是:$e0801$8bWJaSu2IKSn9Z9kM+TPXfOc/9bdYSrN1oD9qfVThWEwdRTnO7re7Ei+fUZRJ68k9lTyuTeUp4of4g24hHnazw==$OAOec05+bXxvuu/1qZ6NUR+xQYvYv7BeL1QxwRpY5Pc=,匹配委托给SCryptPasswordEncoder
⑤sha256的PasswordEncoder,编码密码是:97cde38028ad898ebc02e690819fa220e88c62e0699403e94fff291cfffaf8410849f27605abcbc0,匹配委托给StandardPasswordEncoder

编码Password

idForEncode将传入构造函数中,确认使用哪个PasswordEncoder加密密码。上面构造的DelegatingPasswordEncoder将委托给BCryPtPassword,并以{bcrypt}为前缀。
Example 5. DelegatingPasswordEncoder Encode Example

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

匹配是基于{id}和该id传入构造器的PasswordEncoder的映射来完成。
前面提供了几个密码存储的格式,默认情况下,使用未映射的id(或者空id)调用matches(CharSequence rawPassword, String encodedPassword)将抛出IllegalArgumentException,可以设置一个默认的defaultPasswordEncoderForMatches,使用DelegatingPasswordEncoder.setDefaultPasswordEncoderForMatches(PasswordEncoder)
通过使用id,我们可以匹配任何密码编码,并且使用现代的密码编码技术。这一点很重要,因为与加密不同,密码散列的设计使得无法简单地恢复明文。由于无法恢复明文,因此很难迁移密码。虽然用户迁移NoOpPasswordEncoder很简单,但我们选择在默认情况下包含它,以简化入门体验。

入门体验

如下示例是简单的示例,但这种方式不适应生产环境
Example 6. withDefaultPasswordEncoder Example

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

如果是创建多个用户,可以重用生成器。
Example 7. withDefaultPasswordEncoder Reusing the Builder

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();
使用Spring Boot CLI编码

正确编码的最简单方式是使用Spring Boot CLI
如下示例,将password使用DelegatingPasswordEncoder进行编码:
Example 8. Spring Boot CLI encodepassword Example

spring encodepassword password
{bcrypt}$2a$10$X5wFBtLrL/kHcmrOGGTrGufsBX8CJ0WpQpF3pgeuxBB/H73BK1DW6
问题

当密码存储格式中的id对应的PasswordEncoder 不存在时,会发生异常

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,您将从以下位置迁移密码:

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

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

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

BCryptPasswordEncoder

BCryptPasswordEncoder实现使用广泛支持的bcrypt算法对密码进行散列。为了能够更强的抵抗破解,bcrypt特意将计算速度放慢?与其他自适应单向函数一样,应将其调整为大约1秒来验证系统上的密码。
BCryptPasswordEncoder的默认实现的长度是10,如BCryptPasswordEncoder中的Javadoc所述。
我们鼓励您在自己的系统上调整和测试强度参数,以便验证密码大约需要1秒。
Example 9. BCryptPasswordEncoder

// 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。
Example 10. Argon2PasswordEncoder

// Create an encoder with all the defaults
Argon2PasswordEncoder encoder = new Argon2PasswordEncoder();
String result = encoder.encode("myPassword");
assertTrue(encoder.matches("myPassword", result));
Pbkdf2PasswordEncoder

Pbkdf2PasswordEncoder实现使用PBKDF2算法对密码进行散列。为了防止密码破解,PBKDF2是一种刻意缓慢的算法。与其他自适应单向函数一样,应将其调整为大约1秒来验证系统上的密码。当需要FIPS认证时,该算法是一个很好的选择。
Example 11. Pbkdf2PasswordEncoder

// Create an encoder with all the defaults
Pbkdf2PasswordEncoder encoder = new Pbkdf2PasswordEncoder();
String result = encoder.encode("myPassword");
assertTrue(encoder.matches("myPassword", result));
SCryptPasswordEncoder

SCryptPasswordEncoder实现使用scrypt算法对密码进行散列。为了克服现代硬件上的密码破解,scrypt是一种需要大量内存的刻意缓慢的算法。与其他自适应单向函数一样,应将其调整为大约1秒来验证系统上的密码。
Example 12. SCryptPasswordEncoder

// Create an encoder with all the defaults
SCryptPasswordEncoder encoder = new SCryptPasswordEncoder();
String result = encoder.encode("myPassword");
assertTrue(encoder.matches("myPassword", result));
其他PasswordEncoders

其他PasswordEncoder实现完全是为了向后兼容。它们都被弃用,以表明它们不再被认为是安全的。但是,由于难以迁移现有的遗留系统,因此没有删除它们的计划。

密码存储配置

Spring Security默认使用DelegatingPasswordEncoder,可以使用PasswordEncoder 做为Spring bean来定制。如果您从Spring Security 4.2.x迁移,可以通过公开NoOpPasswordEncoder bean来使用以前的方式。

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

Example 13. NoOpPasswordEncoder

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

XML配置要求NoOpPasswordEncoder bean 的名字是passwordEncoder

修改密码配置

大多数允许用户指定密码的应用程序也需要更新密码的功能。
Well-Know URL 是一种密码修改机制。您可以配置Spring Security以提供此发现端点。例如,如果应用程序中的更改密码端点是/change password,则可以如下配置Spring Security:
Example 14. Default Change Password Endpoint

http
    .passwordManagement(Customizer.withDefaults())

当密码管理器导航到/.well-known/change-password,Spring Security将重定向您的端点到/change-password。
或者,如果您的端点不是/change-password,您也可以这样指定:

http
    .passwordManagement((management) -> management
        .changePasswordPage("/update-password")
    )

使用上述配置,当密码管理器导航到/.well-known/change-password,则Spring Security将重定向到/update-password。