• SpringSecurity系列——密码存储加密策略day7-1(源于官网5.7.2版本)


    Password Storage(密码存储)

    Spring Security 的 PasswordEncoder 接口用于执行密码的单向转换,以允许安全地存储密码。 鉴于 PasswordEncoder 是一种单向转换,当密码转换需要双向转换时(即存储用于向数据库进行身份验证的凭据),则不打算这样做。 通常 PasswordEncoder 用于存储需要在身份验证时与用户提供的密码进行比较的密码

    密码存储策略历史

    多年来,存储密码的标准机制不断发展。 一开始,密码以纯文本形式存储。 密码被认为是安全的,因为数据存储密码保存在访问它所需的凭据中。 但是,恶意用户能够使用 SQL 注入等攻击找到获取用户名和密码的大量“数据转储”的方法。 随着越来越多的用户凭证成为公共安全专家意识到我们需要做更多的工作来保护用户的密码。

    然后鼓励开发人员在通过单向哈希(例如 SHA-256)运行密码后存储密码。 当用户尝试进行身份验证时,哈希密码将与他们键入的密码的哈希值进行比较。 这意味着系统只需要存储密码的单向哈希。 如果发生违规行为,那么只有一种方式的密码散列被暴露。 由于散列是一种方式,并且在给定散列的情况下猜测密码在计算上很困难,因此不值得努力找出系统中的每个密码。 为了打败这个新系统,恶意用户决定创建称为彩虹表的查找表。 他们不是每次都猜测每个密码,而是计算一次密码并将其存储在查找表中。

    为了降低 Rainbow Tables 的有效性,鼓励开发人员使用加盐密码。 不是只使用密码作为散列函数的输入,而是为每个用户的密码生成随机字节(称为盐)。 盐和用户密码将通过产生唯一散列的散列函数运行。 salt 将以明文形式存储在用户密码旁边。 然后,当用户尝试进行身份验证时,哈希密码将与存储的盐的哈希值和他们键入的密码进行比较。 独特的盐意味着彩虹表不再有效,因为每个盐和密码组合的哈希值都不同。

    在现代,我们意识到加密哈希(如 SHA-256)不再安全。 原因是使用现代硬件,我们每秒可以执行数十亿次哈希计算。 这意味着我们可以轻松地单独破解每个密码。

    现在鼓励开发人员利用自适应单向函数来存储密码。 使用自适应单向函数验证密码是有意占用资源(即 CPU、内存等)的。 自适应单向功能允许配置“工作因数”,该“工作因数”会随着硬件变得更好而增长。 建议将“工作因素”调整为大约需要 1 秒来验证系统上的密码。 这种权衡是为了让攻击者难以破解密码,但成本不会太高,它会给您自己的系统带来过多的负担。 Spring Security 试图为“工作因素”提供一个良好的起点,但鼓励用户为自己的系统自定义“工作因素”,因为性能会因系统而异。 应使用的自适应单向函数示例包括 bcrypt、PBKDF2、scrypt 和 argon2。

    由于自适应单向函数有意占用大量资源,因此为每个请求验证用户名和密码会显着降低应用程序的性能。Spring Security(或任何其他库)无法加速密码验证,因为通过使验证资源密集来获得安全性。 鼓励用户将长期凭证(即用户名和密码)交换为短期凭证(即会话、OAuth 令牌等)。 可以快速验证短期凭证,而不会损失任何安全性。

    委托密码编码器

    在 Spring Security 5.0 之前,默认 PasswordEncoder 是 NoOpPasswordEncoder,它需要纯文本密码。 根据 Password History 部分,您可能会认为默认 PasswordEncoder 现在类似于 BCryptPasswordEncoder。 然而,这忽略了三个现实世界的问题:

    1. 有许多应用程序使用无法轻松迁移的旧密码编码
    2. 密码存储的最佳做法将再次改变
    3. 作为一个框架,Spring Security 不能频繁地进行重大更改

    相反,Spring Security 引入了 DelegatingPasswordEncoder,它通过以下方式解决了所有问题:

    1. 确保使用当前密码存储建议对密码进行编码
    2. 允许验证现代和传统格式的密码
    3. 允许将来升级编码

    您可以使用 PasswordEncoderFactories 轻松构造 DelegatingPasswordEncoder 的实例。

    创建默认 DelegatingPasswordEncoder

    PasswordEncoder passwordEncoder =
        PasswordEncoderFactories.createDelegatingPasswordEncoder();
    
    • 1
    • 2

    或者,您可以创建自己的自定义实例。 例如:

    创建自定义 DelegatingPasswordEncoder

    String idForEncode = "bcrypt";
    Map encoders = new HashMap<>();
    encoders.put(idForEncode, new BCryptPasswordEncoder());
    encoders.put("noop", NoOpPasswordEncoder.getInstance());
    encoders.put("pbkdf2", new Pbkdf2PasswordEncoder());
    encoders.put("scrypt", new SCryptPasswordEncoder());
    encoders.put("sha256", new StandardPasswordEncoder());
    
    PasswordEncoder passwordEncoder =
        new DelegatingPasswordEncoder(idForEncode, encoders);
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    Password Storage Format(密码存储格式)

    密码的一般格式是:
    DelegatingPasswordEncoder 存储格式

    {id}encodedPassword
    
    • 1

    这样 id 是用于查找应该使用哪个 PasswordEncoder 的标识符,encodedPassword 是所选 PasswordEncoder 的原始编码密码。 id 必须在密码的开头,以 { 开头,以 } 结尾。 如果找不到 id,则 id 将为空。 例如,以下可能是使用不同 id 编码的密码列表。 所有原始密码都是“密码”。

    DelegatingPasswordEncoder 编码密码示例

    1.{bcrypt}$2a$10$dXJ3SW6G7P50lGmMkkmwe.20cQQubK3.HZWzG3YB1tlRy.fqvM/BG
    2.{noop}password
    3.{pbkdf2}5d923b44a6d129f3ddf3e3c8d29412723dcbde72445e8ef6bf3b508fbf17fa4ed4d6b99ca763d8dc
    4.{scrypt}$e0801$8bWJaSu2IKSn9Z9kM+TPXfOc/9bdYSrN1oD9qfVThWEwdRTnO7re7Ei+fUZRJ68k9lTyuTeUp4of4g24hHnazw==$OAOec05+bXxvuu/1qZ6NUR+xQYvYv7BeL1QxwRpY5Pc=
    5.{sha256}97cde38028ad898ebc02e690819fa220e88c62e0699403e94fff291cfffaf8410849f27605abcbc0
    
    • 1
    • 2
    • 3
    • 4
    • 5
    1. 第一个密码的 PasswordEncoder id 为 bcrypt,encodedPassword 为 $2a$10$dXJ3SW6G7P50lGmMkkmwe.20cQQubK3.HZWzG3YB1tlRy.fqvM/BG。 匹配时它将委托给 BCryptPasswordEncoder
    2. 第二个密码的 PasswordEncoder id 为 noop,encodedPassword 为密码。 匹配时,它将委托给 NoOpPasswordEncoder
    3. 第三个密码的 PasswordEncoder id 为 pbkdf2,encodedPassword 为 5d923b44a6d129f3ddf3e3c8d29412723dcbde72445e8ef6bf3b508fbf17fa4ed4d6b99ca763d8dc。 匹配时,它将委托给 Pbkdf2PasswordEncoder
    4. 第四个密码的 PasswordEncoder id 为 scrypt,encodedPassword 为$e0801$8bWJaSu2IKSn9Z9kM+TPXfOc/9bdYSrN1oD9qfVThWEwdRTnO7re7Ei+fUZRJ68k9lTyuTeUp4of4g24hHnazw==$OAOec05+bXxvuu/1qZ6NUR+xQYvYv7BeL1QxwRpY5Pc= 匹配时,它将委托给 SCryptPasswordEncoder
    5. 最终密码的 PasswordEncoder id 为 sha256,encodedPassword 为 97cde38028ad898ebc02e690819fa220e88c62e0699403e94fff291cfffaf8410849f27605abcbc0。 匹配时,它将委托给 StandardPasswordEncoder

    一些用户可能会担心存储格式是为潜在的黑客提供的。 这不是一个问题,因为密码的存储不依赖于作为秘密的算法。 此外,大多数格式在没有前缀的情况下很容易被攻击者找出。 例如,BCrypt 密码通常以 2 a 2a 2a 开头。

    Password Encoding(密码编码)

    传递给构造函数的 idForEncode 确定将使用哪个 PasswordEncoder 来编码密码。 在我们上面构建的 DelegatingPasswordEncoder 中,这意味着编码密码的结果将委托给 BCryptPasswordEncoder 并以 {bcrypt} 为前缀。 最终结果将如下所示:

    DelegatingPasswordEncoder 编码示例

    {bcrypt}$2a$10$dXJ3SW6G7P50lGmMkkmwe.20cQQubK3.HZWzG3YB1tlRy.fqvM/BG
    
    • 1

    Password Matching(密码匹配)

    匹配是基于 {id} 和 id 到构造函数中提供的 PasswordEncoder 的映射来完成的。 我们在密码存储格式中的示例提供了如何完成此操作的工作示例。 默认情况下,使用密码和未映射的 id(包括空 id)调用 match(CharSequence, String) 的结果将导致 IllegalArgumentException。 可以使用 DelegatingPasswordEncoder.setDefaultPasswordEncoderForMatches(PasswordEncoder) 自定义此行为。

    通过使用 id,我们可以匹配任何密码编码,但使用最现代的密码编码对密码进行编码。 这很重要,因为与加密不同,密码哈希的设计使得没有简单的方法可以恢复明文。 由于无法恢复明文,因此难以迁移密码。 虽然用户迁移 NoOpPasswordEncoder 很简单,但我们选择默认包含它以简化入门体验。

    BCryptPasswordEncoder

    BCryptPasswordEncoder 实现使用广泛支持的 bcrypt 算法来散列密码。 为了使其更能抵抗密码破解,bcrypt 故意放慢了速度。 与其他自适应单向功能一样,应该将其调整为大约 1 秒来验证系统上的密码。 BCryptPasswordEncoder 的默认实现使用 BCryptPasswordEncoder 的 Javadoc 中提到的强度 10。 鼓励您在自己的系统上调整和测试强度参数,以便大约需要 1 秒来验证密码。

    // Create an encoder with strength 16
    BCryptPasswordEncoder encoder = new BCryptPasswordEncoder(16);
    String result = encoder.encode("myPassword");
    assertTrue(encoder.matches("myPassword", result));
    
    • 1
    • 2
    • 3
    • 4

    Change Password Configuration(更改密码配置)

    大多数允许用户指定密码的应用程序也需要更新密码的功能。

    用于更改密码的知名 URL 指示密码管理器可以发现给定应用程序的密码更新端点的机制。

    您可以配置 Spring Security 以提供此发现端点。 例如,如果您的应用程序中的更改密码端点是 /change-password,那么您可以像这样配置 Spring Security:

    http
        .passwordManagement(Customizer.withDefaults())
    
    • 1
    • 2

    然后,当密码管理器导航到 /.well-known/change-password 时,Spring Security 将重定向您的端点 /change-password。

    或者,如果您的端点不是 /change-password,您也可以像这样指定:

    更改密码端点

    http
        .passwordManagement((management) -> management
            .changePasswordPage("/update-password")
        )
    
    • 1
    • 2
    • 3
    • 4

    通过上述配置,当密码管理器导航到 /.well-known/change-password 时,Spring Security 将重定向到 /update-password。

    KeyGenerators(密钥生成器)

    KeyGenerators 类提供了许多方便的工厂方法来构造不同类型的密钥生成器。 使用这个类,您可以创建一个 BytesKeyGenerator 来生成 byte[] 键。 您还可以构造一个 StringKeyGenerator 来生成字符串键。 KeyGenerators 是线程安全的。

    BytesKeyGenerator(字节密钥生成器)

    使用 KeyGenerators.secureRandom 工厂方法生成由 SecureRandom 实例支持的 BytesKeyGenerator:

    BytesKeyGenerator generator = KeyGenerators.secureRandom();
    byte[] key = generator.generateKey();
    
    • 1
    • 2

    默认密钥长度为 8 个字节。 还有一个 KeyGenerators.secureRandom 变体可以控制密钥长度:

    KeyGenerators.secureRandom(16);
    
    • 1

    使用 KeyGenerators.shared 工厂方法构造一个 BytesKeyGenerator,它在每次调用时总是返回相同的键:

    KeyGenerators.shared(16);
    
    • 1

    StringKeyGenerator(字符串密钥生成器)

    使用 KeyGenerators.string 工厂方法构造一个 8 字节的 SecureRandom KeyGenerator,它将每个密钥十六进制编码为字符串

    KeyGenerators.string();
    
    • 1

    修改密码加密实例

    初识密码加密

    我们可以简单写一个测试来进行体验

    BCryptPasswordEncoder的使用

    @Test
        void pwdTest(){
            BCryptPasswordEncoder bCryptPasswordEncoder = new BCryptPasswordEncoder();
            //encode方法进行加密
            String encode = bCryptPasswordEncoder.encode("123456");
            System.out.println("pwd has been encoded:"+encode);
            //match方法进行匹配
            System.out.println(bCryptPasswordEncoder.matches("123",encode));
            System.out.println(bCryptPasswordEncoder.matches("123456",encode));
    
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    默认散列的等级为10,当然我们也可以直接进行修改来进行提高

    BCryptPasswordEncoder bCryptPasswordEncoder = new BCryptPasswordEncoder(15);
    
    • 1

    在这里插入图片描述

    使用KeyGenerators实现随机加密盐

    @Test
        void pwdTest(){
            BCryptPasswordEncoder bCryptPasswordEncoder = new BCryptPasswordEncoder();
            StringKeyGenerator key = KeyGenerators.string();
            System.out.println(key.generateKey());
            //encode方法进行加密
            String encode = bCryptPasswordEncoder.encode("123456"+key);
            System.out.println("pwd has been encoded:"+encode);
            //match方法进行匹配
            System.out.println(bCryptPasswordEncoder.matches("123",encode));
            System.out.println(bCryptPasswordEncoder.matches("123456"+key,encode));
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

    第一次加密验证
    在这里插入图片描述

    第二次加密验证
    在这里插入图片描述
    如你所见两次加密得到的结果是不同的通过这种方式提高安全性

    简单修改加密验证(指定策略方式)

    package com.example.pwdencode.config;
    
    import org.springframework.context.annotation.Bean;
    import org.springframework.context.annotation.Configuration;
    import org.springframework.security.config.Customizer;
    import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity;
    import org.springframework.security.config.annotation.web.builders.HttpSecurity;
    import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
    import org.springframework.security.core.userdetails.User;
    import org.springframework.security.core.userdetails.UserDetails;
    import org.springframework.security.core.userdetails.UserDetailsService;
    import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
    import org.springframework.security.provisioning.InMemoryUserDetailsManager;
    import org.springframework.security.web.SecurityFilterChain;
    
    @EnableMethodSecurity
    @EnableWebSecurity
    @Configuration
    public class SpringSecurityConfig {
    
        @Bean
        public UserDetailsService userDetails(){
            UserDetails build = User.withUsername("user1").password("{bcrypt}$2a$10$laiNvpYlylmnWMhiPjFFHuEfHeZJUY2MlosV1bba4rG.IiadN6Sry").roles("user").build();
            InMemoryUserDetailsManager inMemoryUserDetailsManager = new InMemoryUserDetailsManager(build);
            return inMemoryUserDetailsManager;
        }
    
        @Bean
        public SecurityFilterChain securityFilterChain(HttpSecurity httpSecurity) throws Exception {
            return httpSecurity.csrf().disable()
                    .formLogin(Customizer.withDefaults())
                    .httpBasic(Customizer.withDefaults())
                    .userDetailsService(userDetails())
                    .build();
        }
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37

    如此一来我们可以看到现在的密码是使用{bcrypt}加密的,只要进行指明,系统就能针对加密策略形成匹配策略进行密码的匹配,虽然前端传的还是明文但是后面匹配完美

    简单修改加密验证(更改策略方式)

    使用以下方式我们在SpringSecurityConfig中使用Bean的方式注入PasswordEncoder即可做到上面的例子,这固定了密码的策略,但在创建用户时无需指定其加密策略标志方式
    实际更加推荐上一种,因为更加灵活

    @Bean
        public PasswordEncoder passwordEncoder(){
            return new BCryptPasswordEncoder();
        }
    
    • 1
    • 2
    • 3
    • 4

    完整代码如下

    package com.example.pwdencode.config;
    
    import org.springframework.context.annotation.Bean;
    import org.springframework.context.annotation.Configuration;
    import org.springframework.security.config.Customizer;
    import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity;
    import org.springframework.security.config.annotation.web.builders.HttpSecurity;
    import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
    import org.springframework.security.core.userdetails.User;
    import org.springframework.security.core.userdetails.UserDetails;
    import org.springframework.security.core.userdetails.UserDetailsService;
    import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
    import org.springframework.security.crypto.password.PasswordEncoder;
    import org.springframework.security.provisioning.InMemoryUserDetailsManager;
    import org.springframework.security.web.SecurityFilterChain;
    
    @EnableMethodSecurity
    @EnableWebSecurity
    @Configuration
    public class SpringSecurityConfig {
    
        @Bean
        public PasswordEncoder passwordEncoder(){
            return new BCryptPasswordEncoder();
        }
    
        @Bean
        public UserDetailsService userDetails(){
            UserDetails build = User.withUsername("user1").password("$2a$10$laiNvpYlylmnWMhiPjFFHuEfHeZJUY2MlosV1bba4rG.IiadN6Sry").roles("user").build();
            InMemoryUserDetailsManager inMemoryUserDetailsManager = new InMemoryUserDetailsManager(build);
            return inMemoryUserDetailsManager;
        }
    
        @Bean
        public SecurityFilterChain securityFilterChain(HttpSecurity httpSecurity) throws Exception {
            return httpSecurity.csrf().disable()
                    .formLogin(Customizer.withDefaults())
                    .httpBasic(Customizer.withDefaults())
                    .userDetailsService(userDetails())
                    .build();
        }
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
  • 相关阅读:
    Linux学习-21-yum命令(查询、安装、升级和卸载软件包)和软件组管理
    大数据技术之Hive:先导篇(一)
    linux shell(一)
    $nextTick 原理及作用
    Python学习1(变量、语句、字符串)
    Tomcat 启动闪退问题解决方法
    计算机毕业设计hadoop++hive微博舆情预测 微博舆情分析 微博推荐系统 微博预警系统 微博数据分析可视化大屏 微博情感分析 微博爬虫 知识图谱
    【论文阅读笔记】-针对RSA的短解密指数的密码学分析(Cryptanalysis of Short RSA Secret Exponents)
    狂码两万字!最新八股文(Java岗),建议全文背诵
    利用R语言进行典型相关分析实战
  • 原文地址:https://blog.csdn.net/qq_51553982/article/details/126024678