• security加密解密


    一. 密码加密简介

    1. 散列加密概述

    我们开发时进行密码加密,可用的加密手段有很多,比如对称加密、非对称加密、信息摘要等。在一般的项目里,常用的就是信息摘要算法,也可以被称为散列加密函数,或者称为散列算法、哈希函数。这是一种可以从任何数据中创建数字“指纹”的方法,常用的散列函数有 MD5 消息摘要算法、安全散列算法(Secure Hash Algorithm)等。

    2. 散列加密原理

    散列函数通过把消息或数据压缩成摘要信息,使得数据量变小,将数据的格式固定下来,然后将数据打乱混合,再重新创建成一个散列值,从而达到加密的目的。散列值通常用一个短的随机字母和数字组成的字符串来代表,一个好的散列函数在输入域中很少出现散列冲突。在散列表和数据处理时,如果我们不抑制冲突来区别数据,会使得数据库中的记录很难找到。

    但是仅仅使用散列函数还不够,如果我们只是单纯的使用散列函数而不做特殊处理,其实是有风险的!比如在两个用户密码明文相同时,生成的密文也会相同,这样就增加了密码泄漏的风险。

    所以为了增加密码的安全性,一般在密码加密过程中还需要“加盐”,而所谓的“盐”可以是一个随机数,也可以是用户名。”加盐“之后,即使密码的明文相同,用户生成的密码密文也不相同,这就可以极大的提高密码的安全性。

    传统的加盐方式需要在数据库中利用专门的字段来记录盐值,这个字段可以是用户名字段(因为用户名唯一),也可以是一个专门记录盐值的字段,但这样的配置比较繁琐。

    二、SpringSecurity 中的密码源码分析

    1、PasswordEncoder

    security中加密接口是PasswordEncoder,接口用于执行密码的单向转换,以便安全地存储密码,源码如下

    public interface PasswordEncoder {
    //该方法提供了明文密码的加密处理,加密后密文的格式主要取决于PasswordEncoder接口实现类实例。
        String encode(CharSequence rawPassword);
    //匹配存储的密码以及登录时传递的密码(登录密码是经过加密处理后的字符串)是否匹配,如果匹配该方法则会返回true,第一个参数表示需要被解析的密码 第二个参数表示存储的密码
        boolean matches(CharSequence rawPassword, String encodedPassword);
    
        default boolean upgradeEncoding(String encodedPassword) {
            return false;
        }
    }

    接口实现类列表如下

    举例使用

    Spring Security 5.0之前默认的PasswordEncoder实现类,即默认的加密方案是NoOpPasswordEncoder,5之后这个类已经被标记为过时了,因为不安全(NoOpPasswordEncoderencode方法就只是简单地把字符序列转成字符串)。

    2、DelegatingPasswordEncoder

    2.1、介绍

    Security 5之后用的默认加密方案实现类是DelegatingPasswordEncoder,既然默认密码编码器NoOpPasswordEncoder已经被”不推荐”了,那我们有理由推测现在的默认密码编码器换成了使用某一特定算法的编码器.可是这样便会带来三个问题:

    • 有许多使用旧密码编码的应用程序无法轻松迁移
    • 密码存储的最佳做法(算法)可能会再次发生变化
    • 作为一个框架,Spring Security不能经常发生突变

    DelegatingPasswordEncoder是怎么解决这个问题的,在看解决方法之前先看使用DelegatingPasswordEncoder所能达到的效果:

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

    事实上DelegatingPasswordEncoder并不是传统意义上的编码器,它并不使用某一特定算法进行编码,顾名思义,它是一个委派密码编码器,它将具体编码的实现根据要求委派给不同的算法,以此来实现不同编码算法之间的兼容和变化协调,也就是说它是一个代理类,主要用来代理不同的密码加密方案

    DelegatingPasswordEncoder 构造方法

        public DelegatingPasswordEncoder(String idForEncode,
            Map 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);
        }

    idForEncode决定密码编码器的类型,idToPasswordEncoder决定判断匹配时兼容的类型
    而且idToPasswordEncoder必须包含idForEncode(不然加密后就无法匹配了)

    围绕这个构造方法通常有两种创建思路,如下:

    2.2、创建方式

    第一种:工厂构造

    PasswordEncoder passwordEncoder = PasswordEncoderFactories.createDelegatingPasswordEncoder();

    其具体实现如下:

    public class PasswordEncoderFactories {
        public static PasswordEncoder createDelegatingPasswordEncoder() {
            String encodingId = "bcrypt";//默认
            Map 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);
        }
    
        private PasswordEncoderFactories() {
        }

    这个可以简单地理解为,遇到新密码,DelegatingPasswordEncoder会委托给BCryptPasswordEncoder(encodingId为bcryp*)进行加密,同时,对历史上使用ldap,MD4,MD5等等加密算法的密码认证保持兼容(如果数据库里的密码使用的是MD5算法,那使用matches方法认证仍可以通过,但新密码会使bcrypt进行储存),十分神奇,原理后面会讲

    第二种:定制构造

    接下来是定制构造,其实和工厂方法是一样的,一般情况下推荐直接使用工厂方法,这里给一个小例子

    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);

    3、密码存储格式

    标准的存储格式如下

    {id}encodedPassword

    其中,id标识使用PaswordEncoder的种类
    encodedPassword是原密码被编码后的密码

    注意:

    rawPassword,encodedPassword,密码存储格式(prefixEncodedPassword),这三者是不同的概念!

    rawPassword相当于字符序列”123456”

    encodedPassword是使用id为”mycrypt”对应的密码编码器”123456”编码后的字符串,假设为”qwertyuiop”

    存储的密码 prefixEncodedPassword是在数据库中,我们所能见到的形式,如”{mycrypt}qwertyuiop”

    这个概念在后面讲matches方法的源码时会用到,请留意

    例如rawPasswordpassword在使用不同编码算法的情况下在数据库的存储如下:

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

    {noop}password

    {pbkdf2}5d923b44a6d129f3ddf3e3c8d29412723dcbde72445e8ef6bf3b508fbf17fa4ed4d6b99ca763d8dc

    {scrypt}$e0801$8bWJaSu2IKSn9Z9kM+TPXfOc/9bdYSrN1oD9qfVThWEwdRTnO7re7Ei+fUZRJ68k9lTyuTeUp4of4g24hHnazw==$OAOec05+bXxvuu/1qZ6NUR+xQYvYv7BeL1QxwRpY5Pc=

    {sha256}97cde38028ad898ebc02e690819fa220e88c62e0699403e94fff291cfffaf8410849f27605abcbc0

    这里需要指明,密码的可靠性并不依赖于加密算法的保密,即密码的可靠在于就算你知道我使用的是什么算法你也无法还原出原密码(当然,对于本身就可逆的编码算法来说就不是这样了,但这样的算法我们通常不会认为是可靠的),而且,即使没有标明使用的是什么算法,攻击者也很容易根据一些规律从编码后的密码字符串中推测出编码算法,如bcrypt算法通常是以$2a$开头的

    4、密码编码与匹配

    从上文可知,idForEncode这个构造参数决定使用哪个PasswordEncoder进行密码的编码,编码的方法如下:

    public String encode(CharSequence rawPassword) {
        return "{" + this.idForEncode + "}" + this.passwordEncoderForEncode.encode(rawPassword);
    }

    所以用上文构造的DelegatingPasswordEncoder默认使用BCryptPasswordEncoder,结果格式如

    {bcrypt}2a2a10$dXJ3SW6G7P50lGmMkkmwe.20cQQubK3.HZWzG3YB1tlRy.fqvM/BG

    密码编码方法比较简单,重点在于匹配.匹配方法源码如下:

        @Override
        public boolean matches(CharSequence rawPassword, String prefixEncodedPassword) {
            if(rawPassword == null && prefixEncodedPassword == null) {
                return true;
            }
            //取出编码算法的id
            String id = extractId(prefixEncodedPassword);
            //根据编码算法的id从支持的密码编码器Map(构造时传入)中取出对应编码器
            PasswordEncoder delegate = this.idToPasswordEncoder.get(id);
            if(delegate == null) {
            //如果找不到对应的密码编码器则使用默认密码编码器进行匹配判断,此时比较的密码字符串是 prefixEncodedPassword
                return this.defaultPasswordEncoderForMatches
                    .matches(rawPassword, prefixEncodedPassword);
            }
            //从 prefixEncodedPassword 中提取获得 encodedPassword 
            String encodedPassword = extractEncodedPassword(prefixEncodedPassword);
            //使用对应编码器进行匹配判断,此时比较的密码字符串是 encodedPassword ,不携带编码算法id头
            return delegate.matches(rawPassword, encodedPassword);
        }

    这个匹配方法其实也挺好理解的,唯一需要特别注意的就是找不到对应密码编码器时使用的默认密码编码器,

    我们来看看defaultPasswordEncoderForMatches是一个什么东西

    DelegatingPasswordEncoder的源码里对应内容如下

        private static final String PREFIX = "{";
        private static final String SUFFIX = "}";
        private final String idForEncode;
        private final PasswordEncoder passwordEncoderForEncode;
        private final Map idToPasswordEncoder;
    
        private PasswordEncoder defaultPasswordEncoderForMatches = new UnmappedIdPasswordEncoder();
    
        public void setDefaultPasswordEncoderForMatches(
            PasswordEncoder defaultPasswordEncoderForMatches) {
            if(defaultPasswordEncoderForMatches == null) {
                throw new IllegalArgumentException("defaultPasswordEncoderForMatches cannot be null");
            }
            this.defaultPasswordEncoderForMatches = defaultPasswordEncoderForMatches;
        }
    
        private class UnmappedIdPasswordEncoder implements PasswordEncoder {
    
            @Override
            public String encode(CharSequence rawPassword) {
                throw new UnsupportedOperationException("encode is not supported");
            }
    
            @Override
            public boolean matches(CharSequence rawPassword,
                String prefixEncodedPassword) {
                String id = extractId(prefixEncodedPassword);
                throw new IllegalArgumentException("There is no PasswordEncoder mapped for the id \"" + id + "\"");
            }
        }

    可以看到,DelegatingPasswordEncoder里面,

    PREFIX 和SUFFIX 是常量,

    idForEncode,passwordEncoderForEncode和idToPasswordEncoder是在构造方法中传入决定并不可修改的,

    只有defaultPasswordEncoderForMatches 是有一个setDefaultPasswordEncoderForMatches方法进行设置的可变对象.

    而且,它有一个私有的默认实现UnmappedIdPasswordEncoder,这个所谓的默认实现的唯一作用就是抛出异常提醒你要自己选择一个默认密码编码器来取代它,通常我们只会可能用到它的matches方法,这个时候就会报抛出如下异常

    java.lang.IllegalArgumentException: There is no PasswordEncoder mapped for the id "null"

    遇到这个异常,最简单的做法就是明确提供一个PasswordEncoder对密码进行编码,如果是从Spring Security 5.0 之前迁移而来的,由于之前默认使用的是NoOpPasswordEncoder并且数据库的密码保存格式不带有加密算法id头,会报id为null异常,所以应该明确提供一个NoOpPasswordEncoder密码编码器.

    这里有两种思路,其一就是使用NoOpPasswordEncoder取代DelegatingPasswordEncoder,以恢复到之前版本的状态,这也是笔者在其他博客上看得比较多的一种解决方法.另外就是使用DelegatingPasswordEncoder的setDefaultPasswordEncoderForMatches方法指定默认的密码编码器为NoOpPasswordEncoder,这两种方法孰优孰劣自然不言而喻,官方文档是这么说的

    Reverting to NoOpPasswordEncoder is not considered to be secure. You should instead migrate to using DelegatingPasswordEncoder to support secure password encoding.

    恢复到NoOpPasswordEncoder不被认为是安全的。您应该转而使用DelegatingPasswordEncoder支持安全密码编码

    当然,你也可以将数据库保存的密码都加上一个{noop}前缀,这样DelegatingPasswordEncoder就知道要使用NoOpPasswordEncoder了,这确实是一种方法,但没必要,这里我们来看一下前面的两种解决方法的实现

    第一种:使用NoOpPasswordEncoder取代DelegatingPasswordEncoder

    @Bean

    public static NoOpPasswordEncoder passwordEncoder(){

    return NoOpPasswordEncoder.getInstance();

    }

    第二种:使用DelegatingPasswordEncoder指定defaultPasswordEncoderForMatches

    @Bean

    public static PasswordEncoder passwordEncoder( ){

    DelegatingPasswordEncoder delegatingPasswordEncoder =

    (DelegatingPasswordEncoder) PasswordEncoderFactories.createDelegatingPasswordEncoder();

    //设置defaultPasswordEncoderForMatches为NoOpPasswordEncoder

    delegatingPasswordEncoder.setDefaultPasswordEncoderForMatches(NoOpPasswordEncoder.getInstance());

    return delegatingPasswordEncoder;

    }

    5、项目使用

    5.1、非DelegatingPasswordEncoder方式

    如果我们项目中不需要使用DelegatingPasswordEncoder委托密码编码方式,可以通过@Bean的方式来统一配置全局共用的PasswordEncoder,如下所示:

    @Bean

    public PasswordEncoder passwordEncoder() {

    //可以根据项目自行选择所使用的PasswordEncoder实现类。

    return new BCryptPasswordEncoder();

    }

    使用

    @Controller
    public class Test {
        @Autowired
        PasswordEncoder passwordEncoder;
    
        //模拟注册用户
        @RequestMapping("addUser")
        public  void addUser(User user){
            //$2a$10$zfkqrT3EtxUiRinpMaGvBe.CsVD7YV9DJKURyONO6L4q6LxOd3.cy
            String encode = passwordEncoder.encode("123456");
            user.setPassWord(encode);
            userDao.addUser(user);
        }
    }

    5.2、DelegatingPasswordEncoder方式

    DelegatingPasswordEncoder是默认的PasswordEncoder加密方式,所以我们可以为不同的用户配置所使用不同的密码加密方式,只需要密码格式按照:{away}encodePassword来进行持久化即可。

    @Configuration
    @EnableWebSecurity
    public class WebSecurityConfiguration extends WebSecurityConfigurerAdapter {
     
        @Override
        protected void configure(HttpSecurity http) throws Exception {
            http
                    .formLogin()
                    .and()
                    .csrf()
                    .disable()
                    .authorizeRequests()
                    .antMatchers("/**")
                    .authenticated();
        }
     
        @Bean
        public UserDetailsService users() {
            // {MD5}value必须大写,value值必须是32位小写
            // admin
            UserDetails admin = User.builder()
                    //.passwordEncoder(encoder::encode)
                    .username("admin").password(
                            "{MD5}e10adc3949ba59abbe56e057f20f883e"
                    ).roles("admin").build();
     
            // hengboy
            UserDetails hengboy = User.builder()
                    .username("hengboy")
                    .password("{bcrypt}$2a$10$iMz8sMVMiOgRgXRuREF/f.ChT/rpu2ZtitfkT5CkDbZpZlFhLxO3y")
                    .roles("admin")
                    .build();
     
            // yuqiyu
            UserDetails yuqiyu = User.builder().username("yuqiyu")
                    //.password("{noop}123456")
                    .password("{pbkdf2}cc409867e39f011f6332bbb6634f58e98d07be7fceefb4cc27e62501594d6ed0b271a25fd9f7fc2e")
                    .roles("user").build();
     
            return new InMemoryUserDetailsManager(admin, yuqiyu, hengboy);
        }
    }

    5.3、多密码加密方案共存

    我们进行开发时,经常需要对老旧项目进行改造。这个老旧项目,一开始用的密码加密方案可能是MD5,后来因为种种原因,可能会觉得这个MD5加密不合适,想更新替换一种新的加密方案。但是我们进行项目开发时,密码加密方式一旦确定,基本上没法再改了,毕竟我们不能让用户重新注册再设置一次新密码吧。但是我们此时确实又想使用最新的密码加密方案,那怎么办呢?

    这时候,我们就可以考虑使用DelegatingPasswordEncoder来实现多密码加密方案了!

    首先配置DelegatingPasswordEncoder对象

    @Bean

    public PasswordEncoder passwordEncoder() {

    //利用工厂类PasswordEncoderFactories实现,工厂类内部采用的是委派密码编码方案!

    //推荐使用该方案,因为后期可以实现多密码加密方案共存效果!

    return PasswordEncoderFactories.createDelegatingPasswordEncoder();

    }

    测试接口

    1. @RestController
    2. @RequestMapping("/user")
    3. public class UserController {
    4. @Autowired
    5. private PasswordEncoder passwordEncoder;
    6. @Autowired
    7. private UserMapper userMapper;
    8. @GetMapping("hello")
    9. public String hello() {
    10. return "hello, user";
    11. }
    12. /**
    13. * 采用默认的PasswordEncoder,即BCryptPasswordEncoder来加密。
    14. *
    15. * 添加用户.这里我们采用表单形式传参,传参形式如下:
    16. * http://localhost:8080/user/register?username=test&password=123
    17. */
    18. @GetMapping("/register")
    19. public User registerUser(@RequestParam(required = false) User user) {
    20. user.setEnable(true);
    21. user.setRoles("ROLE_ADMIN");
    22. //对密码进行加密
    23. user.setPassword(passwordEncoder.encode(user.getPassword()));
    24. userMapper.addUser(user);
    25. return user;
    26. }
    27. /**
    28. * 利用MD5加密密码
    29. */
    30. @GetMapping("/registerMd5")
    31. public User registerUserWithMd5(@RequestParam(required = false, name = "username") String username, @RequestParam(required = false, name = "password") String password) {
    32. User user = new User();
    33. user.setUsername(username);
    34. user.setEnable(true);
    35. user.setRoles("ROLE_ADMIN");
    36. Map encoders = new HashMap<>(16);
    37. //encoders.put("bcrypt", new BCryptPasswordEncoder());
    38. //encoders.put("noop", org.springframework.security.crypto.password.NoOpPasswordEncoder.getInstance());
    39. encoders.put("MD5", new org.springframework.security.crypto.password.MessageDigestPasswordEncoder("MD5"));
    40. DelegatingPasswordEncoder md5Encoder = new DelegatingPasswordEncoder("MD5", encoders);
    41. //对密码进行加密
    42. user.setPassword(md5Encoder.encode(password));
    43. userMapper.addUser(user);
    44. return user;
    45. }
    46. /**
    47. * 不进行密码加密
    48. */
    49. @GetMapping("/registerNoop")
    50. public User registerUserWithNoop(@RequestParam(required = false, name = "username") String username, @RequestParam(required = false, name = "password") String password) {
    51. User user = new User();
    52. user.setUsername(username);
    53. user.setEnable(true);
    54. user.setRoles("ROLE_ADMIN");
    55. Map encoders = new HashMap<>(16);
    56. //encoders.put("bcrypt", new BCryptPasswordEncoder());
    57. //encoders.put("MD5", new org.springframework.security.crypto.password.MessageDigestPasswordEncoder("MD5"));
    58. encoders.put("noop", NoOpPasswordEncoder.getInstance());
    59. DelegatingPasswordEncoder noopEncoder = new DelegatingPasswordEncoder("noop", encoders);
    60. //对密码进行加密
    61. user.setPassword(noopEncoder.encode(password));
    62. userMapper.addUser(user);
    63. return user;
    64. }
    65. }

    记得配置类中对上三个接口放行,浏览器中分别请求以上的3个接口,添加3个用户

    我的数据库中,此时就会有3个采用不同加密方案的用户了。

    然后我们可以分别利用这三个用户进行登录,可以发现在同一个项目中,实现了支持3种不同的密码加密方案的效果。

    参考

    Spring Security系列教程22--Spring Security中的密码加密_一一哥Sun的博客-CSDN博客_springsecurity默认加密方式

    Spring Security 5.0的DelegatingPasswordEncoder详解_linshenkx的博客-CSDN博客

  • 相关阅读:
    全面解析TCP协议(三次握手、四次挥手,头部报文格式)
    Python: 打包发布到PyPi上
    FLinkCDC
    ESP32的VScode环境搭建
    佩戴最舒适的蓝牙耳机推荐,不入耳佩戴的骨传导蓝牙耳机
    vscode ctrl+鼠标左键无法跳转
    Prompt Tuning训练过程
    python学习——各种模块argparse、os、sys、time、re、sched、函数体
    Kotlin 协程二三事:入门
    java框架-springmvc
  • 原文地址:https://blog.csdn.net/qq_34491508/article/details/126142543