10.10.2 DelegatingPasswordEncoder-Spring Security用户手册-Java-IT技术博客

10.10.2 DelegatingPasswordEncoder

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

  • 有许多使用旧密码编码的应用程序无法轻松迁移

  • 密码存储的最佳做法将再次更改。

  • 作为一个框架,Spring Security不能经常进行重大更改

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

  • 确保使用当前密码存储建议对密码进行编码

  • 允许以现代和旧式格式验证密码

  • 允许将来升级编码

您可以使用PasswordEncoderFactories轻松构造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);


密码存储格式

密码的一般格式为:

{id}encodedPassword

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

{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的passwordEncoder ID。匹配时将委派给BCryptPasswordEncoder


2第二个密码将具有noop的PasswordEncoder id和密码的encodePassword。匹配时将委派给NoOpPasswordEncoder


3第三个密码的密码编码器ID为pbkdf2,编码密码为5d923b44a6d129f3ddf3e3c8d29412723dcbde72445e8ef6bf3b508fbf17fa4ed4d6b99ca763d8dc。匹配时将委派给Pbkdf2PasswordEncoder


4第四密码将有$e0801$8bWJaSu2IKSn9Z9kM+TPXfOc/9bdYSrN1oD9qfVThWEwdRTnO7re7Ei+fUZRJ68k9lTyuTeUp4of4g24hHnazw==$OAOec05+bXxvuu/1qZ6NUR+xQYvYv7BeL1QxwRpY5Pc=的scrypt和encodedPassword的ID的PasswordEncoder匹配时,将委托给SCryptPasswordEncoder


5最终密码将具有sha256的PasswordEncoder ID和97cde38028ad898ebc02e690819fa220e88c62e0699403e94fff291cfffaf8410849f27605abcbc0的passwordEncoder ID。匹配时将委派给StandardPasswordEncoder


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


密码编码

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

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

密码匹配

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


入门经验

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

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

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

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

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

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

故障排除

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

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恢复到以前的行为。 例如,如果您使用的是Java配置,则可以创建如下所示的配置:

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

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

如果您使用的是XML配置,则可以公开一个ID为passwordEncoder的PasswordEncoder:

<b:bean id="passwordEncoder"
        class="org.springframework.security.crypto.password.NoOpPasswordEncoder" factory-method="getInstance"/>

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

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

to

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

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

标签: Spring SecuritySpring文档Spring Security中文教程SpringSecurity手册