PasswordEncoder详解
Spring Security 中通过 PasswordEncoder 接口定义了密码加密和比对的相关操作:
public interface PasswordEncoder {
String encode(CharSequence rawPassword);
boolean matches(CharSequence rawPassword, String encodedPassword);
default boolean upgradeEncoding(String encodedPassword) {
return false;
}
}
可以看到,PasswordEncoder 接口中一共有三个方法:
-
encode:该方法用来对明文密码进行加密。
-
matches:该方法用来进行密码比对。
-
upgradeEncoding:该方法用来判断当前密码是否需要升级,默认返回 false 表示不需要升级。
针对密码的所有操作,PasswordEncoder 接口中都定义好了,不同的实现类将采用不同的密码加密方案对密码进行处理。
PasswordEncoder常见实现类
BCryptPasswordEncoder
BCryptPasswordEncoder 使用 bcrypt 算法对密码进行加密,为了提高密码的安全性,bcrypt 算法故意降低运行速度,以增强密码破解的难度。同时 BCryptPasswordEncoder “为自己带盐”,开发者不需要额外维护一个 “盐” 字段,使用 BCryptPasswordEncoder 加密后的字符串就已经 “带盐” 了,即使相同的明文每次生成的加密字符串者都不相同。
BCryptPasswordEncoder 的默认强度为 10,开发者可以根据自己的服务器性能进行调整,以确保密码验证时间约为 1 秒钟(官方建议密码验证时间为 1 秒钟,这样既可以提高系统安全性,又不会过多影响系统运行性能)
Argon2PasswordEncoder
Argon2PasswordEncoder 使用 Argon2 算法对密码进行加密,Argon2 曾在 Password Hashing Competition 竞赛中获胜。为了解决在定制硬件上密码容易被破解的问题,Argon2 也是故意降低运算速度,同时需要大量内存,以确保系统的安全性。
Pbkdf2PasswordEncoder
Pbkdf2PasSWordEncoder 使用 PBKDF2 算法对密码进行加密,和前面几种类似,PBKDF2 算法也是一种故意降低运算速度的算法,当需要 FIPS(Federal Information Processing Standard,美国联邦信息处理标准)认证时,PBKDF2 算法是一个很好的选择。
SCryptPasswordEncoder
SCryptPasswordEncoder 使用 scrypt 算法对密码进行加密,和前面的几种类似,scrypt 也是,一种故意降低运算速度的算法,而且需要大量内存。
这四种就是我们前面所说的自适应单向函数加密。除了这几种,还有一些基于消息摘要算法的加密方案,这些方案都已经不再安全,但是出于兼容性考虑,Spring Security 并未移除相关类,主要有 LdapShaPasswordEncoder、MessageDigestPasswordEncoder、Md4PasswordEncoder、StandardPasswordEncoder 以及 NoOpPasswordEncoder(密码明文存储),这五种皆已废弃,这里对这些类也不做过多介绍。
除了上面介绍的这几种之外,还有一个非常重要的密码加密工具类,那就是,DelegatingPasswordEncoder。
DelegatingPasswordEncoder
根据前文的介绍,读者可能会认为 Spring Security 中默认的密码加密方案应该是四种自适应单向加密函数中的一种,其实不然,在 Spring Security 5.0 之后,默认的密码加密方案其实是 DelegatingPasswordEncoder。
从名字上来看,DelegatingPasswordEncoder 是一个代理类,而并非一种全新的密码加密方案。DelegatingPasswordEncoder 主要用来代理上面介绍的不同的密码加密方案。为什么采用 DelegatingPasswordEncoder 而不是某一个具体加密方式作为默认的密码加密方案呢?主要考虑了如下三方面的因素:
-
兼容性:使用 DelegatingPasswordEncoder 可以帮助许多使用旧密码加密方式的系统顺利迁移到 Spring Security 中,它允许在同一个系统中同时存在多种不同的密码加密方案。
-
便捷性:密码存储的最佳方案不可能一直不变,如果使用 DelegatingPasswordEncoder 作为默认的密码加密方案,当需要修改加密方案时,只需要修改很小一部分代码就可以实现。
-
稳定性:作为一个框架,Spring Security 不能经常进行重大更改,而使用 DelegatingPasswordEncoder 可以方便地对密码进行升级(自动从一个加密方案升级到另外一个加密方案)。
那么 DelegatingPasswordEncoder 到底是如何代理其他密码加密方案的?又是如何对加密方案进行升级的?我们就从 PasswordEncoderFactories 类开始看起,因为正是由它里边的静态方法 createDelegatingPasswordEncoder 提供了默认的 DelegatingPasswordEncoder 实例:
public class PasswordEncoderFactories {
@SuppressWarnings("deprecation")
public static PasswordEncoder createDelegatingPasswordEncoder() {
String encodingId = "bcrypt";
Map<String, PasswordEncoder> encoders = new HashMap<>();
encoders.put(encodingId, new BCryptPasswordEncoder());
encoders.put("ldap", new org.springframework.security.crypto.password.LdapShaPasswordEncoder());
encoders.put("MD4", new org.springframework.security.crypto.password.Md4PasswordEncoder());
encoders.put("MD5", new org.springframework.security.crypto.password.MessageDigestPasswordEncoder("MD5"));
encoders.put("noop", org.springframework.security.crypto.password.NoOpPasswordEncoder.getInstance());
encoders.put("pbkdf2", new Pbkdf2PasswordEncoder());
encoders.put("scrypt", new SCryptPasswordEncoder());
encoders.put("SHA-1", new org.springframework.security.crypto.password.MessageDigestPasswordEncoder("SHA-1"));
encoders.put("SHA-256", new org.springframework.security.crypto.password.MessageDigestPasswordEncoder("SHA-256"));
encoders.put("sha256", new org.springframework.security.crypto.password.StandardPasswordEncoder());
encoders.put("argon2", new Argon2PasswordEncoder());
return new DelegatingPasswordEncoder(encodingId, encoders);
}
private PasswordEncoderFactories() {}
}
可以看到,在 createDelegatingPasswordEncoder 方法中,首先定义了 encoders 变量,encoders 中存储了每一种密码加密方案的 id 和所对应的加密类,例如 bcrypt 对应着 BcryptPasswordEncoder、argon2 对应着 Argon2PasswordEncoder、noop 对应着 NoOpPasswordEncoder。
encoders 创建完成后,最终新建一个 DelegatingPasswordEncoder 实例,并传入 encodingId 和 encoders 变量,其中 encodingId 默认值为 bcrypt,相当于代理类中默认使用的加密方案是 BCryptPasswordEncoder。
我们来分析一下 DelegatingPasswordEncoder 类的源码,由于源码比较长,我们就先从它的属性开始看起:
public class DelegatingPasswordEncoder implements PasswordEncoder {
private static final String PREFIX = "{";
private static final String SUFFIX = "}";
private final String idForEncode;
private final PasswordEncoder passwordEncoderForEncode;
private final Map<String, PasswordEncoder> idToPasswordEncoder;
private PasswordEncoder defaultPasswordEncoderForMatches = new UnmappedIdPasswordEncoder();
}
-
首先定义了前缀 PREFIX 和后缀 SUFFIX,用来包裹将来生成的加密方案的 id。
-
idForEncode 表示默认的加密方案 id。
-
passwordEncoderForEncode 表示默认的加密方案(BCryptPasswordEncoder),它的值是根据 idForEncode 从 idToPasswordEncoder 集合中提取出来的。
-
idToPasswordEncoder 用来保存 id 和加密方案之间的映射。
-
defaultPasswordEncoderForMatches 是指默认的密码比对器,当根据密码加密方案的 id 无法找到对应的加密方案时,就会使用默认的密码比对器。 defaultPasswordEncoderForMatches 的默认类型是 UnmappedIdPasswordEncoder,在 UnmappedIdPasswordEncoder 的 matches 方法中并不会做任何密码比对操作,直接抛出异常。
-
最后看到的 DelegatingPasswordEncoder 也是 PasswordEncoder 接的子类,所以接下来我们就来重点分析 PasswordEncoder 接口中三个方法在 DelegatingPasswordEncoder 中的具体实现。
首先来看 encode 方法:
@Override
public String encode(CharSequence rawPassword) {
return PREFIX + this.idForEncode + SUFFIX + this.passwordEncoderForEncode.encode(rawPassword);
}
encode 方法的实现逻辑很简单,具体的加密工作还是由加密类来完成,只不过在密码加密完成后,给加密后的字符串加上一个前缀 {id}
,用来描述所采用的具体加密方案。因此,encode 方法加密出来的字符串格式类似如下形式:
{bcrypt}$2as10$dXJ3SW6G7P50lGmMkkmwe.20cQQubK3.HzWzG3YB1tlRy.fqvM/BG
{noop}123
{pbkdf2}23b44a6d129f3ddf3e3c8d29412723dcbde72445e8ef6bf3b508fbf17fa4ed4
不同的前缀代表了后面的字符串采用了不同的加密方案。
再来看密码比对方法 matches:
@Override
public boolean matches(CharSequence rawPassword, String prefixEncodedPassword) {
if (rawPassword == null && prefixEncodedPassword == null) {
return true;
}
String id = extractId(prefixEncodedPassword);
PasswordEncoder delegate = this.idToPasswordEncoder.get(id);
if (delegate == null) {
return this.defaultPasswordEncoderForMatches
.matches(rawPassword, prefixEncodedPassword);
}
String encodedPassword = extractEncodedPassword(prefixEncodedPassword);
return delegate.matches(rawPassword, encodedPassword);
}
private String extractId(String prefixEncodedPassword) {
if (prefixEncodedPassword == null) {
return null;
}
int start = prefixEncodedPassword.indexOf(PREFIX);
if (start != 0) {
return null;
}
int end = prefixEncodedPassword.indexOf(SUFFIX, start);
if (end < 0) {
return null;
}
return prefixEncodedPassword.substring(start + 1, end);
}
在 matches 方法中,首先调用 extractId 方法从加密字符串中提取出具体的加密方案 id,也就是 {}
中的字符,具体的提取方式就是字符串截取。拿到 id 之后,再去 idToPasswordEncoder 集合中获取对应的加密方案,如果获取到的为 null,说明不存在对应的加密实例,那么就会采用默认的密码匹配器 defaultPasswordEncoderForMatches; 如果根据 id 获取到了对应的加密实例,则调用其 matches 方法完成密码校验。
可以看到,这里的 matches 方法非常灵活,可以根据加密字符串的前缀,去查找到不同的加密方案,进而完成密码校验。同一个系统中,加密字符串可以使用不同的前缀而互不影响。
最后,我们再来看一下 DelegatingPasswordEncoder 中的密码升级方法 upgradeEncoding:
@Override
public boolean upgradeEncoding(String prefixEncodedPassword) {
String id = extractId(prefixEncodedPassword);
if (!this.idForEncode.equalsIgnoreCase(id)) {
return true;
}
else {
String encodedPassword = extractEncodedPassword(prefixEncodedPassword);
return this.idToPasswordEncoder.get(id).upgradeEncoding(encodedPassword);
}
}
可以看到,如果当前加密字符串所采用的加密方案不是默认的加密方案(BcryptPasswordEncoder),就会自动进行密码升级,否则就调用默认加密方案的 upgradeEncoding 方法判断密码是否需要升级。
至此,我们将 Spring Security 中的整个加密体系向读者简单介绍了一遍,接下来我们通过几个实际的案例来看一下加密方案要怎么用。