相关文章:
- OAuth2的定义和运行流程
- Spring Security OAuth实现Gitee快捷登录
- Spring Security OAuth实现GitHub快捷登录
- Spring Security的过滤器链机制
- Spring Security OAuth Client配置加载源码分析
- Spring Security内置过滤器详解
- 为什么加载了两个OAuth2AuthorizationRequestRedirectFilter分析
- Spring Security 自定义授权服务器实践
- Spring Security 自定义资源服务器实践
- 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
能简单的构造出DelegatingPasswordEncoder
Example 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,编码密码是:$2adXJ3SW6G7P50lGmMkkmwe.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。