【深入浅出 Spring Security(六)】一文搞懂密码的加密和比对

【深入浅出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 则解决了所有问题:

扫描二维码关注公众号,回复: 15491461 查看本文章
  • 确保使用的密码编码可以进行规范的正确的密码存储;
  • 允许以现代和遗留格式验证密码;
  • 允许将来升级编码;

源码分析

先了解其属性成员

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实例,内部属性可以看看。

在这里插入图片描述

猜你喜欢

转载自blog.csdn.net/qq_63691275/article/details/131038686