1.Spring Security5中,出于安全性考虑调整了PasswordEncoder的实现与使用策略,原先的StandardPasswordEncoder,LdapShaPasswordEncoder,Md4PasswordEncoder,MessageDigestPasswordEncoder,
NoOpPasswordEncoder均@Deprecated过时,推荐使用BCryptPasswordEncoder

2.查看源码找到加密(encode)和密码匹配(matches)方法

3.在spring security中大家应该还记得有个注入Bean,贴代码:

	@Bean
	public PasswordEncoder passwordEncoder() {
	
	    return new BCryptPasswordEncoder(); 
	}          

4.在此处的时候就疑惑过加密的过程,因为我密码一开始是明文存储,后来使用的Bcrypt.hashpw()加密后修改密码的,但是依然可以从数据库取出用户信息认证通过,查看BCryptPasswordEncoder源码:

 public String encode(CharSequence rawPassword) {
        String salt;
        if (this.random != null) {
            salt = BCrypt.gensalt(this.version.getVersion(), this.strength, this.random);
        } else {
            salt = BCrypt.gensalt(this.version.getVersion(), this.strength);
        }
        return BCrypt.hashpw(rawPassword.toString(), salt);
  }      

5.可以看到BCryptPasswordEncoder.encode()的实现其实用的就是BCrypt.hashpw(rawPassword.toString(), salt),所以在之前的userDetailService中的loadUserByUsername()才能认证成功。至此疑惑解除

6.接下来继续研究BCryptPasswordEncoder,采用BCrypt强hash算法,使用者可以选择使用或者使用:版本号($2a,$2b,$2y)、长度、SecureRandom实例;长度参数越长,密码的hash要做的工作越多(指数增长),长度默认是10,加密后的hash串开头默认为$2a。

随机salt的获取:BCrypt.gensalt();查看源码:

	  public static String gensalt() {
	      return gensalt(10);
	  }
	  public static String gensalt(int log_rounds) throws IllegalArgumentException {
	        return gensalt(log_rounds, new SecureRandom());
	  }
	  public static String gensalt(int log_rounds, SecureRandom random) throws IllegalArgumentException {
	        return gensalt("$2a", log_rounds, random);
	  }     

7.BCrypt是Niels Provos和DavidMazières基于Blowfish加密算法设计的密码哈希算法,于1999年在USENIX协会上提交。Bcrypt在设计上包含了一个盐Salt来防御彩虹表攻击,还提供了一种自适应功能,可以随着时间的推移,通过增加迭代计数以使其执行更慢,使得即便在增加计算能力的情况下,Bcrypt仍然能保持抵抗暴力攻击。
Bcrypt是OpenBSD和SUSE Linux等操作系统默认的密码哈希算法。但是在使用Bcrypt算法的实现时,要注意它有最大密码长度限制,通常为50~72字符,准确的长度限制取决于具体的Bcrypt实现。超过最大长度的密码将被截断。

8.解密BCryptPasswordEncoder的matches()方法:

  public boolean matches(CharSequence rawPassword, String encodedPassword) {
        if (encodedPassword != null && encodedPassword.length() != 0) {
            if (!this.BCRYPT_PATTERN.matcher(encodedPassword).matches()) {
                this.logger.warn("Encoded password does not look like BCrypt");
                return false;
            } else {
                return BCrypt.checkpw(rawPassword.toString(), encodedPassword);
            }
        } else {
            this.logger.warn("Empty encoded password");
            return false;
        }
  }      

9.执行之前先判断被加密的encodedPassword是否是BCrypt加密后的hash形式,不是的话返回false,是的话进入下一步BCrypt.checkpw(rawPassword.toString(), encodedPassword):

  public static boolean checkpw(String plaintext, String hashed) {
        return equalsNoEarlyReturn(hashed, hashpw(plaintext, hashed));
  }

  public static String hashpw(String password, String salt) {
        byte[] passwordb = password.getBytes(StandardCharsets.UTF_8);
        return hashpw(passwordb, salt);
  }

  public static String hashpw(byte[] passwordb, String salt) {
        char minor = 0;
        StringBuilder rs = new StringBuilder();
        if (salt == null) {
            throw new IllegalArgumentException("salt cannot be null");
        } else {
            int saltLength = salt.length();
            //判断密码长度,小于28,hash错误
            if (saltLength < 28) {
                throw new IllegalArgumentException("Invalid salt");
            } else if (salt.charAt(0) == '$' && salt.charAt(1) == '2') {
                //判断hash是否以$2开头
                byte off;
                if (salt.charAt(2) == '$') {
                    //salt.charAt(2)=='a' || 'b' || 'y',不等于'$'
                    off = 3;
                } else {
                    //minor='a';
                    minor = salt.charAt(2);
                    //minor应该是查salt reversion, 'a','b','y' 都能找到, 'x'目前没找到,
                    if (minor != 'a' && minor != 'x' && minor != 'y' && minor != 'b' || salt.charAt(3) != '$') {
                        throw new IllegalArgumentException("Invalid salt revision");
                    }

                    off = 4;
                }
                //salt.chatAt(6) 应该是Bcrypt+长度后的字符'$',$2a$10$,第三个'$'应该是标识算法的结束符,自己感觉哈
                if (salt.charAt(off + 2) > '$') {
                    throw new IllegalArgumentException("Missing salt rounds");
                } else if (off == 4 && saltLength < 29) {
                    throw new IllegalArgumentException("Invalid salt");
                } else {
                    //off=4,取出saltRounds,默认10次
                    int rounds = Integer.parseInt(salt.substring(off, off + 2));
                    //off=4,截取salt.substring(7,29),取出salt
                    String real_salt = salt.substring(off + 3, off + 25);
                    //走到这有点蒙圈,算法不太能看懂,不过初步已经知道怎么matches(a,b)了
                    //应该是取出hash里salt,将输入的明文password使用Bcrypt.hashpw(password,salt)加密之后
                    /**
                    * public static boolean checkpw(String plaintext, String hashed) {
        			*		return equalsNoEarlyReturn(hashed, hashpw(plaintext, hashed));
    				*	}
                    *
                    */
                    /**
                    *static boolean equalsNoEarlyReturn(String a, String b) {
        			*	return MessageDigest.isEqual(a.getBytes(StandardCharsets.UTF_8), b.getBytes(StandardCharsets.UTF_8));
    				*	}
                    *
                    */
                    byte[] saltb = decode_base64(real_salt, 16);
                    if (minor >= 'a') {
                        passwordb = Arrays.copyOf(passwordb, passwordb.length + 1);
                    }
                    BCrypt B = new BCrypt();
                    byte[] hashed = B.crypt_raw(passwordb, saltb, rounds, minor == 'x', minor == 'a' ? 65536 : 0);
                    rs.append("$2");
                    if (minor >= 'a') {
                        rs.append(minor);
                    }
                    rs.append("$");
                    if (rounds < 10) {
                        rs.append("0");
                    }
                    rs.append(rounds);
                    rs.append("$");
                    encode_base64(saltb, saltb.length, rs);
                    encode_base64(hashed, bf_crypt_ciphertext.length * 4 - 1, rs);
                    return rs.toString();
                }
            } else {
                throw new IllegalArgumentException("Invalid salt version");
            }
        }
    }         

10.跟到此处之后对Bcrypt算法更加熟悉了,最后附上Bcrypt的密码图解

Bcrypt密码图解

11.谢谢观看

Logo

旨在为数千万中国开发者提供一个无缝且高效的云端环境,以支持学习、使用和贡献开源项目。

更多推荐