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

news2024/12/21 19:15:41

Spring Security 中的密码加密

  • 一、PasswordEncoder 详解
    • 常见的实现类(了解)
    • DelegatingPasswordEncoder
      • 源码分析
      • DelegatingPasswordEncoder 在哪实例化的?
  • 二、自定义加密
    • 自定义方式一:使用{id}的形式
    • 自定义方式二:向Spring容器中注入PasswordEncoder对象
  • 三、总结

在【深入浅出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实例,内部属性可以看看。

在这里插入图片描述

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/612047.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

程序员失业转行送外卖,晒出当天收入,还以为看错了!

在程序员的共识中&#xff0c;30岁之前自己是很受企业欢迎的&#xff0c;有经验有技术&#xff0c;能够为公司创造足够多的价值。 但是一旦超过了35岁&#xff0c;如果没有做到架构师或者成为管理人员&#xff0c;那很可能是连工作都找不到的。而且这个年龄的程序员还要面临着…

700MHz设备对广播电视信号的干扰有哪些?

700MHz&#xff0c;由于其较长的波长&#xff0c;良好的传播与覆盖特性&#xff0c;不仅一直被多国用作广播电视信号频率&#xff0c;4G LTE和5G NR也同样看好这一频段&#xff0c;并在此频段上进行了相应的部署和规划。目前已经有超过45个国家和地区&#xff0c;将700MHz频段部…

【网络安全】企业应急响应基础技能

windows 任务计划列表 1. 计算机管理窗口,选择 系统工具 中 任务计划程序 中的 任务计划程序库选项 可以查看任务计划的名称,状态,触发器等详细信息 2.powershell中输入get-scheduledtask 可以查看当前系统所有任务计划信息 任务路径,名称,状态等详细信息 3.命令行中输入s…

C++11:右值引用,实现移动语义和完美转发

目录 1、右值引用 2、移动语义&#xff08;std::move&#xff09; 3、完美转发&#xff08;std::forward&#xff09; 1、右值引用 右值引用&#xff08;Rvalue reference&#xff09;是C11引入的一个新特性&#xff0c;它是一种新的引用类型&#xff0c;用于表示将要被移动…

5个小时,搭出2套应用,这一低代码平台很强劲!

现代管理学之父德鲁克提及创新本质时&#xff0c;说了两点&#xff1a; 一是让昂贵的东西变得便宜&#xff0c;老百姓能用&#xff1b;二是让高门槛东西变得低门槛&#xff0c;普通人可用。 而低代码正符合这两个条件。 一、背景 所谓低代码&#xff0c;是一种软件开发方法&…

常用的LED显示屏驱动芯片和控制系统

常用的LED显示屏驱动芯片包括以下几种&#xff1a; TPIC6B595&#xff1a;这是一种串行输入、并行输出的LED显示屏驱动芯片&#xff0c;适用于驱动7段数码管等简单的LED显示屏。 MAX7219/MAX7221&#xff1a;这是一种常用的LED显示屏驱动器&#xff0c;可驱动8x8点阵LED显示屏。…

npm 发布新包或者新模块后,无法下载最新版本,如何解决?

目录 1、方法一&#xff1a;在npm官网搜索对应的模块&#xff0c;看看是否有最新版本的存在&#xff1f; 2、方法二&#xff1a;排查本地使用的是什么镜像&#xff1f; 3、方法三&#xff1a;将淘宝镜像切换成npm镜像 1、方法一&#xff1a;在npm官网搜索对应的模块&#xf…

运维小白必学篇之基础篇第十四集:DHCP中继实验

DHCP中继实验 目录 服务器端&#xff1a;&#xff08;vmware5&#xff09; 中继器端&#xff1a;(双网卡ens33、vmware5&#xff1b;ens36、vmware6&#xff09; 客户端&#xff1a;&#xff08;vmware6&#xff09; 实验作业&#xff08;主机名为自己的名字&#xff09;&a…

WebGIS学习-01-GIS基础概念与Mapbox基础

1.地图数据来源 1.栅格数据&#xff1a; -.jpg&#xff0c;.png等图片数据&#xff1b; -卫星等拍摄的影像&#xff1b;.tiff 2.矢量数据&#xff1a; -geojson的数据&#xff0c;多用于绘制边界 -放大缩小都不会失真&#xff0c;且高度支持手绘 2.网页是如何渲染地图数据的 …

什么是压力测试?如何进行Jmeter压力测试

一、什么是压力测试 软件测试中&#xff1a;压力测试&#xff08;Stress Test&#xff09;&#xff0c;也称为强度测试、负载测试。压力测试是模拟实际应用的软硬件环境及用户使用过程的系统负荷&#xff0c;长时间或超大负荷地运行测试软件&#xff0c;来测试被测系统的性能、…

深度学习-调参技巧总结

针对CNN优化的总结 使用没有 batchnorm 的 ELU 非线性或者有 batchnorm 的 ReLU。用类似1*1的网络结构预训练RGB数据&#xff0c; 能得到更好的效果。使用线性学习率衰退策略。使用平均和最大池化层的和。使用大约 128&#xff08;0.005&#xff09; 到 256 &#xff08;0.01&a…

5.1 - Web漏洞 - XSS漏洞详解

「作者简介」&#xff1a;CSDN top100、阿里云博客专家、华为云享专家、网络安全领域优质创作者 「推荐专栏」&#xff1a;对网络安全感兴趣的小伙伴可以关注专栏《网络安全入门到精通》 XSS漏洞 一、什么是XSS&#xff1f;二、XSS概述三、靶场练习四、XSS使用步骤五、XSS攻击类…

el-table动态表头数据渲染

desc&#xff1a;el-table的数据是后端动态返回的&#xff0c;包括表头的情况下如何进行渲染 // 第一页的数据 {"code": 1,"message": "查询成功","data": {"current": 1,"size": 10,"total": 41,"…

企业如何通过CRM系统做好客户管理?

企业如何通过CRM系统做好客户管理&#xff1f; CRM客户管理作为一种综合性的管理思想&#xff0c;可以帮助我们更好地了解客户需求和行为&#xff0c;制定更加精准的销售和服务策略&#xff0c;提高客户满意度和忠诚度&#xff0c;从而实现可持续发展。 接下来将给大家详细介…

SQL——事务

&#x1f388; 什么是事务 &#x1f4a7; 概念 事务是用于保证数据的一致性,它由一组相关的DML&#xff08;增、删、改&#xff09;语句&#xff0c;该组的DML语句要么全部成功&#xff0c;要么全部失败。使用事务可以确保数据库的一致性和完整性&#xff0c;避免数据出现异常…

“微商城”项目(5登录和注册)

1.我的信息 在pages\User.vue文件中编写HTML结构代码&#xff0c;示例代码如下。 <template><div class"member"><div class"header-con"><router-link :to"{ name: login }" class"mui-navigate-right">&l…

段 寄 存 器-汇编复习(5)

图解演示8086CPU CS执行过程和逻辑 段 寄 存 器 8086CPU 在访问内存时要由相关部件提供内存单元的段地址和偏移地址,送入地址加法器合成物理地址。这里,需要看一下,是什么部件提供段地址。段地址在 8086CPU 的段寄存器中存放。8086CPU 有 4 个段存器: CS、DS、SS、ES。当80…

力扣刷题笔记——动态规划

动态规划基础 简称DP&#xff0c;如果某⼀问题有很多重叠⼦问题&#xff0c;使⽤动态规划是最有效的。 动态规划中每⼀个状态⼀定是由上⼀个状态推导出来的 1. 确定dp 数组&#xff08; dp table &#xff09;以及下标的含义 2. 确定递推公式 3. dp 数组如何初始化 4. 确定遍…

深度学习的分割方法

FCN&#xff1a;基于深度学习的语义分割模型 语义分割的定义&#xff1a;对像素进行精细化的分类。 用深度学习来解决语义分割&#xff0c;所面临的主要问题是&#xff1a; 早期的深度模型用于分类&#xff0c;输出一维向量&#xff0c;无法分割 深度模型不够精细 动机 如…

谈谈互联网广告拍卖机制的发展:从GSP到DeepAuction

广告作为各互联网公司收入的大头&#xff0c;其拍卖机制设计因此也是关乎营收最为核心的方面。所谓的广告拍卖机制设计是指如何将有限的广告位分配给合适的广告&#xff0c;从而达到客户、平台以及用户三方的价值最优。 当前的广告拍卖被建模为暗拍的形式&#xff0c;即N个广告…