本文参考自Spring Security 5.0.4.RELEASE 的官方文档,结合源码介绍了 DelegatingPasswordEncoder,对其工作过程进行分析并解决其中遇到的问题。包括 There is no PasswordEncoder mapped for the id “null” 非法参数异常的正确处理方法。
PasswordEncoder
首先要理解 DelegatingPasswordEncoder 的作用和存在意义,明白官方为什么要使用它来取代原先的 NoOpPasswordEncoder。
DelegatingPasswordEncoder 和 NoOpPasswordEncoder 都是 PasswordEncoder 接口的实现类。根据官方的定义,Spring Security 的 PasswordEncoder 接口用于执行密码的单向转换,以便安全地存储密码。
关于密码存储的演变历史这里我不多做介绍,简单来说就是现在数据库存储的密码基本都是经过编码的,而决定如何编码以及判断未编码的字符序列和编码后的字符串是否匹配就是 PassswordEncoder 的责任。
这里我们可以看一下 PasswordEncoder 接口的源码:
public interface PasswordEncoder { /** * Encode the raw password. Generally, a good encoding algorithm applies a SHA-1 or * greater hash combined with an 8-byte or greater randomly generated salt. */ String encode(CharSequence rawPassword); /** * Verify the encoded password obtained from storage matches the submitted raw * password after it too is encoded. Returns true if the passwords match, false if * they do not. The stored password itself is never decoded. * * @param rawPassword the raw password to encode and match * @param encodedPassword the encoded password from storage to compare with * @return true if the raw password, after encoding, matches the encoded password from * storage */ boolean matches(CharSequence rawPassword, String encodedPassword); }
根据源码,我们可以直观地看到 PassswordEncoder 接口只有两个方法,一个是 String encode(CharSequence rawPassword),用于将字符序列(即原密码)进行编码;另一个方法是 boolean matches(CharSequence rawPassword, String encodedPassword),用于比较字符序列和编码后的密码是否匹配。
理解了 PasswordEncoder 的作用后我们来 Spring Security 5.0 之前默认 PasswordEncoder 实现类 NoOpPasswordEncoder。这个类因为不安全已经被标记为过时了。下面就让我们来看看它是如何地不安全的:
1 NoOpPasswordEncoder
事实上,NoOpPasswordEncoder 就是没有编码的编码器,源码如下:
@Deprecated public final class NoOpPasswordEncoder implements PasswordEncoder { public String encode(CharSequence rawPassword) { return rawPassword.toString(); } public boolean matches(CharSequence rawPassword, String encodedPassword) { return rawPassword.toString().equals(encodedPassword); } /** * Get the singleton {@link NoOpPasswordEncoder}. */ public static PasswordEncoder getInstance() { return INSTANCE; } private static final PasswordEncoder INSTANCE = new NoOpPasswordEncoder(); private NoOpPasswordEncoder() { } }
可以看到,NoOpPasswordEncoder 的 encode 方法就只是简单地把字符序列转成字符串。也就是说,你输入的密码 ”123456” 存储在数据库里仍然是 ”123456”,这样如果数据库被攻破的话密码就直接泄露了,十分不安全。而且 NoOpPasswordEncoder 也就失去了所谓密码编码器的意义了。
不过正因其十分简单,在 Spring Security 5.0 之前 NoOpPasswordEncoder 是作为默认的密码编码器而存在到,它可以是你没有主动加密时的一个默认选择。
另外,NoOpPasswordEncoder 的实现是一个标准的饿汉单例模式,关于单例模式可以看这一篇文章:单例模式及其4种推荐写法和3类保护手段。
2 DelegatingPasswordEncoder
通过上面的学习我们可以知道,随着安全要求的提高之前的默认密码编码器 NoOpPasswordEncoder 已经被 “不推荐”了,那我们有理由推测现在的默认密码编码器换成了使用某一特定算法的编码器。可是这样便会带来三个问题:
- 有许多使用旧密码编码的应用程序无法轻松迁移;
- 密码存储的最佳做法(算法)可能会再次发生变化;
- 作为一个框架,Spring Security 不能经常发生突变。
简单来说,就是新的密码编码器和旧密码的兼容性、自身的稳健性以及需要一定的可变性(切换到更好的算法)。听起来是不是十分矛盾?那我们就来看看 DelegatingPasswordEncoder 是怎么解决这个问题的。在看解决方法之前先看使用 DelegatingPasswordEncoder 能达到的效果:
1 构造方法
下面我们来看看 DelegatingPasswordEncoder 的构造方法
public DelegatingPasswordEncoder(String idForEncode, Map<String, PasswordEncoder> idToPasswordEncoder) { if(idForEncode == null) { throw new IllegalArgumentException("idFo