PasswordEncoder
是Spring Security
提供的一个接口,称它为密码解析器,这个接口主要是处理密码的。源码如下:
public interface PasswordEncoder {
/**
* Encode the raw password. Generally, a good encoding algorithm applies a SHA-1 or
* greater hash combined with an 8-byte or greater randomly generated salt.
*/
String encode(CharSequence rawPassword);
/**
* Verify the encoded password obtained from storage matches the submitted raw
* password after it too is encoded. Returns true if the passwords match, false if
* they do not. The stored password itself is never decoded.
*
* @param rawPassword the raw password to encode and match
* @param encodedPassword the encoded password from storage to compare with
* @return true if the raw password, after encoding, matches the encoded password from
* storage
*/
boolean matches(CharSequence rawPassword, String encodedPassword);
/**
* Returns true if the encoded password should be encoded again for better security,
* else false. The default implementation always returns false.
* @param encodedPassword the encoded password to check
* @return true if the encoded password should be encoded again for better security,
* else false.
*/
default boolean upgradeEncoding(String encodedPassword) {
return false;
}
}
接口提供3个方法,第一个方法是对明文密码进行加密的,返回一个密文。第二个方法是匹配明文密码和密文,返回布尔值。第三个方法是对密文进行二次加密,这个方法是默认的。
PasswordEncoder
接口有很多实现类,其中最主要的是官方推荐的BCryptPasswordEncoder
类,平时使用的最多的就是这个密码解析器。BCryptPasswordEncoder
是对bcrypt
强散列方法的具体实现,是基于hash算法的单向加密。可以通过strength
来控制强度,默认是10
。
源码如下:
encode方法是对明文密码进行加密,原理是使用一个随机生成的salt,用明文密码加上这个salt来一起进行加密,返回密文,由于这个salt每次生成的都不一样,所以即使明文密码一样,最后加密出来的密文是不一样的,这样保证了用户密码的安全。
matchs方法是用来匹配明文密码和密文的,最终结果用布尔值返回。
测试加密和匹配:
@Test
public void test1() {
String password = "123456";// 密码
PasswordEncoder passwordEncoder = new BCryptPasswordEncoder();
for (int i = 1; i <= 5; i++) {
// 加密明文密码,返回密文
String encoder = passwordEncoder.encode(password);
// 明文和密文进行匹配
boolean bool = passwordEncoder.matches(password, encoder);
System.out.println(encoder + ":是否匹配?" + bool);
}
$2a$10$qcnrAAaJn0g4rnnBc0nz2emAwLXQPe8QYEVbN/YITEFpUZbCH.Pru
:是否匹配?true
$2a$10$BN.YJOmTuHHhj279Lr1r/ue8G9iaYO62y7cjmeonD3toCit.uNfAG
:是否匹配?true
$2a$10$3niTKuqRNbP/8DDAVCw8f.rOyzurdZ1.W0CQAucn8pCf2sihsUkE.
:是否匹配?true
$2a$10$1fTRlKe3YDgSFnSdqgujw.h32cwjtNubcSgCtdc9mNjfGrEtoeJDi
:是否匹配?true
$2a$10$6AIf3GBhWt9WjXc55mObfuRFhn1eyJKOeOdzprg65fmbsWeUrJGdm
:是否匹配?true
可以看到,一样的密码,5次加密后的密文全都不一样,但是全都能匹配上。
这是什么原因呢?继续观察源码:
在明文密码匹配加密密码时,会调用checkpw
函数,再看一下函数源码
查看hashpw源码:
/**
* Hash a password using the OpenBSD bcrypt scheme
* @param passwordb the password to hash, as a byte array
* @param salt the salt to hash with (perhaps generated
* using BCrypt.gensalt)
* @return the hashed password
*/
public static String hashpw(byte passwordb[], String salt) {
BCrypt B;
String real_salt;
byte saltb[], hashed[];
char minor = (char) 0;
int rounds, off;
StringBuilder rs = new StringBuilder();
if (salt == null) {
throw new IllegalArgumentException("salt cannot be null");
}
int saltLength = salt.length();
if (saltLength < 28) {
throw new IllegalArgumentException("Invalid salt");
}
if (salt.charAt(0) != '$' || salt.charAt(1) != '2')
throw new IllegalArgumentException ("Invalid salt version");
if (salt.charAt(2) == '$')
off = 3;
else {
minor = salt.charAt(2);
if ((minor != 'a' && minor != 'x' && minor != 'y' && minor != 'b')
|| salt.charAt(3) != '$')
throw new IllegalArgumentException ("Invalid salt revision");
off = 4;
}
// Extract number of rounds
if (salt.charAt(off + 2) > '$')
throw new IllegalArgumentException ("Missing salt rounds");
if (off == 4 && saltLength < 29) {
throw new IllegalArgumentException("Invalid salt");
}
rounds = Integer.parseInt(salt.substring(off, off + 2));
//上面都是验证salt的有效性,real_salt表示密文里保存着加密的真实salt,截取出来后,将明文密码进行加密返回。之后使用
// equalsNoEarlyReturn 函数 进行匹配验证
real_salt = salt.substring(off + 3, off + 25);
saltb = decode_base64(real_salt, BCRYPT_SALT_LEN);
if (minor >= 'a') // add null terminator
passwordb = Arrays.copyOf(passwordb, passwordb.length + 1);
B = new BCrypt();
hashed = B.crypt_raw(passwordb, saltb, rounds, minor == 'x', minor == 'a' ? 0x10000 : 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();
}
再查看一下生成salt的函数源码:
这说明salt生成后,在区间off + 3, off + 25
上也保存着base64
加密的salt。
结果:
注册时密码由
BCrypt
加密保存到数据库,密文密码中某个区间内保存着此次hash
的salt
,由于salt
是随机生成的,就算明文密码相同密文密码也不一致,在登录验证时将明文密码使用数据库中获得的密文密码中解析到的salt
进行hash
,进而匹配密码是否正确。