文章目录
回顾
上一篇文章《循序渐进学spring security 第八篇,如何配置密码加密?是否支持多种加密方案?》我们介绍了配置密码的加密,顺带介绍了spring security 是支持多种加密方案的,那么,spring security是怎么支持多种加密方案的,我们通过阅读源码发现是通过DelegatingPasswordEncoder 这个实现了PasswordEncoder 的类完成的,今晚我们继续来探索DelegatingPasswordEncoder 是怎么支持多种加密方案的
在阅读本文之前,建议先看看我之前写的文章,有助于理解本文
- 面试不要在说不熟悉spring security了,一个demo让你使劲忽悠面试官
- 循序渐进学习spring security 第二篇,如何修改默认用户?
- 循序渐进学习spring security 第三篇,如何自定义登录页面?登录回调?
- 循序渐进雪spring security 第四篇,登录流程是怎样的?登录用户信息保存在哪里?
- 循序渐进学习spring security 第五篇,如何处理重定向和服务器跳转?登录如何返回JSON串?
- 循序渐进学spring security第六篇,手把手教你如何从数据库读取用户进行登录验证,mybatis集成
- 循序渐进学spring security 第七篇,如何基于用户表和权限表配置权限?越学越简单了
- 循序渐进学spring security 第八篇,如何配置密码加密?是否支持多种加密方案?
为什么要支持多密码方案?
为什么我们会有这种需求?其实这个主要是针对老旧项目改造用的,密码加密方式一旦确定,基本上没法再改了(你总不能让用户重新注册一次吧),但是我们又想使用最新的框架来做密码加密,那么无疑,DelegatingPasswordEncoder 是最佳选择。
DelegatingPasswordEncoder 类是怎么创建的?
上一章节我们介绍了DelegatingPasswordEncoder 是在DaoAuthenticationProvider 初始化时就默认创建了一个PasswordEncoder 的实现,也就是这个时候就创建了DelegatingPasswordEncoder
跟进看源码
public static PasswordEncoder createDelegatingPasswordEncoder() {
String encodingId = "bcrypt";
Map<String, PasswordEncoder> encoders = new HashMap();
encoders.put(encodingId, new BCryptPasswordEncoder());
encoders.put("ldap", new LdapShaPasswordEncoder());
encoders.put("MD4", new Md4PasswordEncoder());
encoders.put("MD5", new MessageDigestPasswordEncoder("MD5"));
encoders.put("noop", NoOpPasswordEncoder.getInstance());
encoders.put("pbkdf2", new Pbkdf2PasswordEncoder());
encoders.put("scrypt", new SCryptPasswordEncoder());
encoders.put("SHA-1", new MessageDigestPasswordEncoder("SHA-1"));
encoders.put("SHA-256", new MessageDigestPasswordEncoder("SHA-256"));
encoders.put("sha256", new StandardPasswordEncoder());
encoders.put("argon2", new Argon2PasswordEncoder());
return new DelegatingPasswordEncoder(encodingId, encoders);
}
也就是说DaoAuthenticationProvider 在初始化时,就创建了DelegatingPasswordEncoder 作为默认PasswordEncoder ,如果我们在配置spring security时没有指定PasswordEncoder 的bean实例时,就会用默认的DelegatingPasswordEncoder实例,而DelegatingPasswordEncoder通过构造方法,第一个参数是encodingId,第二个参数就是一个map集合,map集合中有多个PasswordEncoder的实现,这里要注意的是encodingId 对应的PasswordEncoder 实例是BCryptPasswordEncoder 对象
好了
我们在跟进DelegatingPasswordEncoder的构造方法
public DelegatingPasswordEncoder(String idForEncode, Map<String, PasswordEncoder> idToPasswordEncoder) {
if (idForEncode == null) {
throw new IllegalArgumentException("idForEncode cannot be null");
}
if (!idToPasswordEncoder.containsKey(idForEncode)) {
throw new IllegalArgumentException(
"idForEncode " + idForEncode + "is not found in idToPasswordEncoder " + idToPasswordEncoder);
}
for (String id : idToPasswordEncoder.keySet()) {
if (id == null) {
continue;
}
if (id.contains(PREFIX)) {
throw new IllegalArgumentException("id " + id + " cannot contain " + PREFIX);
}
if (id.contains(SUFFIX)) {
throw new IllegalArgumentException("id " + id + " cannot contain " + SUFFIX);
}
}
this.idForEncode = idForEncode;
this.passwordEncoderForEncode = idToPasswordEncoder.get(idForEncode);
this.idToPasswordEncoder = new HashMap<>(idToPasswordEncoder);
}
在DelegatingPasswordEncoder 构造方法中,第一个参数是idForEncode ,第二个参数是idToPasswordEncoder ,就是Map<String, PasswordEncoder>集合
- 首选判断第一个参数:idForEncode 是否为空,在第二个参数map的集合中是否存在以idForEncode为key的数据,如果不存在,则抛出异常
- 循环遍历idToPasswordEncoder 的key,判断key中是否包含前缀和后缀分别为"{“和”}"的,如果包含,就抛出异常
- 给成员变量赋值,其实,成员变量idForEncode 就是默认的加密方式的key,成员变量passwordEncoderForEncode 就是默认PasswordEncoder加密的实例,而对应的实例,实际上就是BCryptPasswordEncoder对象,也就是说,DelegatingPasswordEncoder 默认的加密对象就是BCryptPasswordEncoder,idToPasswordEncoder 其实还是第二个参数传入的所有数据集合
接下来,我们来看看DelegatingPasswordEncoder 是如何对密码加密的
DelegatingPasswordEncoder 如何加密?
@Override
public String encode(CharSequence rawPassword) {
return PREFIX + this.idForEncode + SUFFIX + this.passwordEncoderForEncode.encode(rawPassword);
}
加密前,先将idForEncode 加上前缀和后缀,再拼接上passwordEncoderForEncode 加密的结果,这就是DelegatingPasswordEncoder 的加密方式。注意的是passwordEncoderForEncode 加密的字符串,才是最原始的密码的加密
DelegatingPasswordEncoder 如何校验密码?
@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);
}
看源码一目了然
- 首先,参数rawPassword 是用户输入的登录密码,prefixEncodedPassword 是系统中的原密码,先从prefixEncodedPassword 中解析出来id,如下是源码,也就是将前缀和后缀中的id取出来作为id
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);
}
- 根据解析出来的id,从idToPasswordEncoder map集合中取出来加密对象delegate ,如果为空,则进入到默认的密码校验方式,其实就是抛出异常,这一步大家可以自己跟进去看源码
- 进一步从原密码中提取出来密码,也就是将以SUFFIX作为分割符取出来原始密码
private String extractEncodedPassword(String prefixEncodedPassword) {
int start = prefixEncodedPassword.indexOf(SUFFIX);
return prefixEncodedPassword.substring(start + 1);
}
- 最后,再根据2中得到的delegate 对象,进行登录密码和原始密码的校验
好了,看源码就非常清晰了
示例代码
接下来,我们尝试使用DelegatingPasswordEncoder 内部的map集合中初始化的几种加密方式对密码进行加密,然后再结合DelegatingPasswordEncoder 的密码加密和密码校验方式,进行一步步校验和证实spring security支持多种加密方式共存的原则
@Test
public void testPasswordEncoder() {
Map<String, PasswordEncoder> encoders = new HashMap<>();
encoders.put("bcrypt", new BCryptPasswordEncoder());
encoders.put("MD5", new MessageDigestPasswordEncoder("MD5"));
encoders.put("SHA-256", new MessageDigestPasswordEncoder("SHA-256"));
encoders.put("noop", NoOpPasswordEncoder.getInstance());
DelegatingPasswordEncoder encoder1 = new DelegatingPasswordEncoder("bcrypt", encoders);
DelegatingPasswordEncoder encoder2 = new DelegatingPasswordEncoder("MD5", encoders);
DelegatingPasswordEncoder encoder3 = new DelegatingPasswordEncoder("SHA-256", encoders);
DelegatingPasswordEncoder encoder4 = new DelegatingPasswordEncoder("noop", encoders);
String e1 = encoder1.encode("123456");
String e2 = encoder2.encode("123456");
String e3 = encoder3.encode("123456");
String e4 = encoder4.encode("123456");
System.out.println("e1 = " + e1);
System.out.println("e2 = " + e2);
System.out.println("e3 = " + e3);
System.out.println("e4 = " + e4);
}
生成密码结果
e1 = {
bcrypt}$2a$10$Udax6Xnpqj/3yduBANRcDOSIQN6Ty.MA2PtQgdO2lDvc9KG39q6GC
e2 = {
MD5}{
1nKIz3ZSAkCw1xDIPmehd5hQNtezarKrfpHsuqM11pY=}ba2563fcddbd2a96d19e4b6b7f5a3693
e3 = {
SHA-256}{
BaQGLNLZLKAPa0OXluSCNmd6dradJN0WAFvScNrs4Wo=}f01de9b47600d7583d6944c7d560e322eaf40dc37e779bb0f30b9e0109b74ae9
e4 = {
noop}123456
有了密码,我们基于之前的文章《循序渐进学spring security 第七篇,如何基于用户表和权限表配置权限?越学越简单了》进行改造,如果没有看过这篇文章的建议先看下,熟悉下项目,或者下载下来自己修改测试
步骤如下:
-
将配置类SecurityConfig中关于PasswordEncoder bean的配置注释,不配置,这样默认就会自动使用DaoAuthenticationProvider 默认的密码代理类DelegatingPasswordEncoder 为默认密码加密类了
-
将用户表中,用户名为harry的密码改为e1 打印的密码,mike 密码改为e2打印的密码,再加steven 用户和james用户,密码分别是是e3和e4打印的密码
-
启动项目,测试上述4个用户的登录情况,发现4个用户不同加密方式的都能正常登录成功
由此可见,想要支持多种加密方案,使用spring security 默认的即可,不需要单独配置PasswordEncoder bean