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的密码图解
11.谢谢观看