在【Spring Security 实战(三)】默认登录认证的实现原理 中小编阐述了在登录认证时,默认情况下,是在 DaoAuthenticationProvider
中的 additionalAuthenticationChecks
方法中进行密码认证的,但没有具体说怎么认证的。本文就具体说说密码的加密和比对吧。
一、PasswordEncoder 详解
在Spring Security 中,PasswordEncoder 是一个接口,具体源码如下。
public interface PasswordEncoder {
// 该方法对明文密码进行加密
String encode(CharSequence rawPassword);
// 该方法用来进行密码比对,明文和密文比对
boolean matches(CharSequence rawPassword, String encodedPassword);
// 该方法用来判断当前密码是否需要升级,可以看见这个方法是默认的
// 默认返回值是 false
default boolean upgradeEncoding(String encodedPassword) {
return false;
}
}
从源码中可以发现,Spring Security 是通过 PasswordEncoder 去实现密码的加密和比对的。俩必要的抽象方法:
- encode:对明文密码进行加密;
- matches:进行密码比对。
常见的实现类(了解)
-
NoOpPasswordEncoder
-
没有加密,就是明文
-
BCryptPasswordEncoder
-
使用 bcrypt 算法对密码进行了加密,为了提高密码的安全性,bcrypt 算法故意降低运行速度,以增强密码破解的难度。同时 BCryptPasswordEncoder 为自己加盐,开发者不需要额外维护一个 盐 字段,使用 BCryptPasswordEncoder 加密后的字符串就已经带盐了,即使使用铭文每次生成的加密字符串的不相同。
-
Argon2PasswordEncoder
-
Pbkdf2PasswordEncoder
-
SCryptPasswordEncoder
咱下面就只拿 BCryptPasswordEncoder 进行事例说明,所以就解释上面俩吧,不多说了。
DelegatingPasswordEncoder
DelegatingPassword 是 PasswordEncoder 的一个实现类,也是自 Spring Security 5 之后默认使用的加密方案。
从其类名上看,我们可以初步判定它内部用了委派(Delegate)设计模式,它主要是根据实际密文委派实际的密码比对方案。
委派模式(Delegate Pattern)又叫委托模式,是一种面向对象的设计模式,允许对象组合实现与 继承相同的代码重用。它的基本作用就是负责任务的调用和分配任务,是一种特殊的静态代理,可以理 解为全权代理,但是代理模式注重过程,而委派模式注重结果。委派模式属于行为型模式,不属于 GOF 23 种设计模式中。
————————————————
通俗地说就是:让一委派对象去判断用哪个对象去处理这个业务,就是让委派对象去做抉泽。通常类名中带有Delegate或Dispatcher的就用了这种设计模式。
很多人奇怪为什么早期使用的 NoOpPasswordEncoder
不直接改为 BCryptPasswordEncoder
,而是选择了 DelegatingPasswordEncoder
。下面是官方文档中给出如果那样改变会有什么麻烦:
- 有很多应用程序使用旧的密码编码不容易进行迁移;
- 密码存储的最佳实践就被更改了;
- 而 Spring Security 作为一个框架而言,不能这么轻易地带破坏性的更改。
而换成DelegatingPasswordEncoder
则解决了所有问题:
- 确保使用的密码编码可以进行规范的正确的密码存储;
- 允许以现代和遗留格式验证密码;
- 允许将来升级编码;
源码分析
先了解其属性成员
public class DelegatingPasswordEncoder implements PasswordEncoder {
// 默认包裹id的前缀
private static final String DEFAULT_ID_PREFIX = "{";
//默认包裹id的后缀
private static final String DEFAULT_ID_SUFFIX = "}";
// 实际包裹id的前缀
private final String idPrefix;
// 实际包裹 id 的后缀
private final String idSuffix;
// 实际加密的id
private final String idForEncode;
// 实际加密的方案对象
private final PasswordEncoder passwordEncoderForEncode;
// 用来委托时候的方案映射
private final Map<String, PasswordEncoder> idToPasswordEncoder;
// 密码对比方案对象
private PasswordEncoder defaultPasswordEncoderForMatches;
}
根据调试可以发现,默认构造后的各个属性初始化结果如下:
根据上面的调试结果,DelegatingPasswordEncoder 根据 {id} 委派方案时可以有如下选择:
DelegatingPasswordEncoder 的加密实现,实际就是用 BCryptPasswordEncoder 去进行加密后的结果。
public String encode(CharSequence rawPassword) {
return this.idPrefix + this.idForEncode + this.idSuffix + this.passwordEncoderForEncode.encode(rawPassword);
}
咱再看看它内部比对密码的实现,实际这里就进行了委派,委派正确的方案,然后再让委派后的对象进行比对。下面对源码进行了注释,可以看看。
public boolean matches(CharSequence rawPassword, String prefixEncodedPassword) {
// rawPassword 是原密码,就咱用户输入的
// prefixEncodePassword 可以理解为是保存在数据库中的密码
if (rawPassword == null && prefixEncodedPassword == null) {
return true;
} else {
// 去获取{id}中的id
String id = this.extractId(prefixEncodedPassword);
// 根据 id 获取实际方案PasswordEncoder
PasswordEncoder delegate = (PasswordEncoder)this.idToPasswordEncoder.get(id);
if (delegate == null) {
return this.defaultPasswordEncoderForMatches.matches(rawPassword, prefixEncodedPassword);
} else {
// 然后如果委派成功就进行比对
String encodedPassword = this.extractEncodedPassword(prefixEncodedPassword);
return delegate.matches(rawPassword, encodedPassword);
}
}
}
DelegatingPasswordEncoder 在哪实例化的?
实际上在 AuthenticationConfiguration
中配置 AuthenticationManagerBuilder
,将其载入到了 AuthenticationBuilder 实例化对象中了。
从上面并未看出它在哪实例化的,但可以看见创建了一个 LazyPasswordEncoder,它是一个静态内部类,里面有个getPasswordEncoder方法,里面用了单例,私有化的方法,LazyPasswordEncoder 里的encode、mathches等方法都是依赖这个单例对象去进行的。可以看见它是先去调用 getBeanOrNull
这个方法去获取PasswordEncoder对象(从Spring容器中
),如果不存在,就去构造 DelegatingPasswordEncoder。在它里面通过 PasswordEncoderFactories 工厂构造的 DelegatingPasswordEncoder。
下面是DefaultPasswordEncoderAuthenticationManagerBuilder 内部类(继承了AuthenticationManagerBuilder)重写的三个 UserDetailsService 配置的方法。passwordEncoder
方法实际是给 DaoAuthentionProvider 中的 赋值…了解了解就好了
其实在实例化 DaoAuthenticaitonProvider 的时候,也会对 passwordEncoder 进行初始化,同样是通过 PasswordEncoderFactories 进行构造的 DelegatingPasswordEncoder。根据上面重写的方法可以发现最后还是会换成 defaultPasswordEncoder,也就是 LazyPasswordEncoder 实例。
综上所述:实际注入形式有两种,一种是采用默认的,从PasswordEncoderFactories 中实例化对象;另一种是向Spring容器中注入自己想使用的PasswordEncoder。
二、自定义加密
通过上面对Spring Security 5 之后默认使用的 PasswordEncoder(DelegatingPasswordEncoder)的源码分析,相信下面对自定义加密的两种方式会很轻松的掌握并理解。
自定义方式一:使用{id}的形式
从上面的源码分析我们可以知道,默认的DelegatingPasswordEncoder 会去找密码前面的 id ,去委派方案进行比对密码,所以我们的密码在前面加上想要匹配的方案 id ,就可以了。这种方式呢,比较灵活,可以根据自己想要的比对方式去配 id 即可,密码形式比较灵活。缺点就是对应方案 id 咱也不好记,所以记住有 PasswordEncoderFactories
这么一个类小编自认为是很有必要的。
方案id ,我们在PasswordEncoderFactories中查找到,如下:
测试案例:
写个测试案例,看看 123 用 BCrypt 加密后的结果。
@Test
public void test(){
// 输出:$2a$10$0BBFHiDx9jmix3FldDvFYexYGrOascxKcDagaG1wW7LpnPeQIjBca
System.out.println(new BCryptPasswordEncoder().encode("123"));
}
然后在配置 UserDetailsService 中进行配置一下。
@Bean
public InMemoryUserDetailsManager inMemoryUserDetailsManager(){
return new InMemoryUserDetailsManager(
User.withUsername("root")
.password("{bcrypt}$2a$10$0BBFHiDx9jmix3FldDvFYexYGrOascxKcDagaG1wW7LpnPeQIjBca")
.roles("admin")
.build());
}
测试结果
自定义方式二:向Spring容器中注入PasswordEncoder对象
上面有对 LazyPasswordEncoder 中的 getPasswordEncoder 方法进行源码分析,说实际是先从Spring容器中找,没有的话再通过PasswordEncoderFactories进行构建。所以我们直接创建一个交给 Spring 容器就可以实现自定义了。但是这种方式不好的地方就是只能用一种 PasswordEncoder 进行密码比对,好处就是专一、密码前不用写{id}。
测试案例:
配置代码
@EnableWebSecurity
public class SecurityConfig {
@Bean
public PasswordEncoder passwordEncoder(){
return new BCryptPasswordEncoder();
}
@Bean
public InMemoryUserDetailsManager inMemoryUserDetailsManager(){
return new InMemoryUserDetailsManager(
User.withUsername("root")
.password("$2a$10$0BBFHiDx9jmix3FldDvFYexYGrOascxKcDagaG1wW7LpnPeQIjBca")
.roles("admin")
.build());
}
@Bean
public AuthenticationManager authenticationManager(HttpSecurity http) throws Exception {
return http
.getSharedObject(AuthenticationManagerBuilder.class)
.userDetailsService(inMemoryUserDetailsManager())
.and()
.build();
}
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
return http.authorizeRequests()
.anyRequest()
.authenticated()
.and()
.formLogin()
.loginProcessingUrl("/api/auth/login")
.usernameParameter("username")
.passwordParameter("password")
.and()
.logout()
.logoutUrl("/api/auth/logout")
.and()
.csrf()
.disable()
.build();
}
}
测试
三、总结
-
有些会有疑问:我不写 {id} 呢?会怎么密码比对呢?其实源码分析的时候很清楚了,如果没有匹配到委派方案,就会按DelegatingPasswordEncoder 中的默认方案,即会抛出java.lang.IllegalArgumentException: There is no PasswordEncoder mapped for the id "null"的异常。
-
DelegatingPasswordEncoder 实例对象是通过 PasswordEncoderFactories 类中的 createPasswordEncoder 方法进行创建的。方法内可以查看对应的 id ,所以这个类需要记住。
-
自定义加密通过源码分析可以得出两种自定义的方案:
-
使用 {id} 的形式,即在密码前加上 {id},例如:{noop}123,即表示等密码比对的时候用 NoOpPasswordEncoder 中的matches方法进行比对。
-
向 Spring 容器中注入 PasswordEncoder 实例,这样Spring Security 会选择注入的实例去进行密码的比对,缺点是整个项目的密码比对都只能采用这种比对方案。
-
对两种自定义加密方式有疑惑的,可以看上面对DefaultPasswordEncoderAuthenticationManagerBuilder 、LazyPasswordEncoder 的源码分析。在LazyPasswordEncoder中解释了getPasswordEncoder方法,是先从Spring容器中找,再去拿 PasswordEncoderFactories 工厂去创建。
下面是咱从 HttpSecurity 中获取的AuthenticationManagerBuilder实例,内部属性可以看看。