BCryptPasswordEncoder加密和對密碼驗證的原理
上一篇:spring security進階2 添加賬戶並對賬戶密碼進行加密
spring security中提供了一個加密類BCryptPasswordEncoder,可以用來對密碼字符串進行加密,得到加密后的字符串。它采用哈希算法 SHA-256 +隨機鹽+密鑰對密碼進行加密
一、加密算法和hash算法的區別
加密算法是一種可逆的算法,基本過程就是對原來為明文的文件或數據按某種算法進行處理,使其成為不可讀的一段代碼為“密文”,但在用相應的密鑰進行操作之后就可以得到原來的內容 。
哈希算法是一種不可逆的算法,是把任意長度的輸入通過散列算法變換成固定長度的輸出,輸出就是散列值,不同的輸入可能會散列成相同的輸出,所以不可能從散列值來確定唯一的輸入值。
二、源碼解析
BCryptPasswordEncoder類實現了PasswordEncoder接口,這個接口中定義了兩個方法
public interface PasswordEncoder {
String encode(CharSequence rawPassword);
boolean matches(CharSequence rawPassword, String encodedPassword);
}
其中encode(...)是對字符串進行加密的方法,matches使用來校驗傳入的明文密碼rawPassword是否和加密密碼encodedPassword相匹配的方法。即對密碼進行加密時調用encode,登錄認證時調用matches
下面我們來看下BCryptPasswordEncoder類中這兩個方法的具體實現
1. encode方法
public String encode(CharSequence rawPassword) {
String salt;
if (strength > 0) {
if (random != null) {
salt = BCrypt.gensalt(strength, random);
}
else {
salt = BCrypt.gensalt(strength);
}
}
else {
salt = BCrypt.gensalt();
}
return BCrypt.hashpw(rawPassword.toString(), salt);
}
可以看到,這個方法中先基於某種規則得到了一個鹽值,然后在調用BCrypt.hashpw方法,傳入明文密碼和鹽值salt。所以我們再看下BCrypt.hashpw方法中做了什么
2. BCrypt.hashpw方法
public static String hashpw(String password, String salt) throws IllegalArgumentException {
BCrypt B;
String real_salt;
byte passwordb[], saltb[], hashed[];
char minor = (char) 0;
int rounds, off = 0;
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' || salt.charAt(3) != '$') {
throw new IllegalArgumentException("Invalid salt revision");
}
off = 4;
}
if (saltLength - off < 25) {
throw new IllegalArgumentException("Invalid salt");
}
// Extract number of rounds
if (salt.charAt(off + 2) > '$') {
throw new IllegalArgumentException("Missing salt rounds");
}
rounds = Integer.parseInt(salt.substring(off, off + 2));
real_salt = salt.substring(off + 3, off + 25);
try {
passwordb = (password + (minor >= 'a' ? "\000" : "")).getBytes("UTF-8");
}
catch (UnsupportedEncodingException uee) {
throw new AssertionError("UTF-8 is not supported");
}
saltb = decode_base64(real_salt, BCRYPT_SALT_LEN);
B = new BCrypt();
hashed = B.crypt_raw(passwordb, saltb, rounds);
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得到real_salt
,后續的操作都是用這個real_salt來進行,最終得到加密字符串。
所以這里有一個重點:傳入的鹽值salt
並不是最終用來加密的鹽,方法中通過salt得到了real_salt
,記住這一點,因為后邊的匹配方法matches中要用到這一點。
3. matches方法
matches方法用來判斷一個明文是否和一個加密字符串對應。
public boolean matches(CharSequence rawPassword, String encodedPassword) {
if (encodedPassword == null || encodedPassword.length() == 0) {
logger.warn("Empty encoded password");
return false;
}
if (!BCRYPT_PATTERN.matcher(encodedPassword).matches()) {
logger.warn("Encoded password does not look like BCrypt");
return false;
}
return BCrypt.checkpw(rawPassword.toString(), encodedPassword);
}
這個方法中先對密文字符串進行了一些校驗,如果不符合規則直接返回不匹配,然后調用校驗方法BCrypt.checkpw,第一個參數是明文,第二個參數是加密后的字符串。
public static boolean checkpw(String plaintext, String hashed) {
return equalsNoEarlyReturn(hashed, hashpw(plaintext, hashed));
}
static boolean equalsNoEarlyReturn(String a, String b) {
char[] caa = a.toCharArray();
char[] cab = b.toCharArray();
if (caa.length != cab.length) {
return false;
}
byte ret = 0;
for (int i = 0; i < caa.length; i++) {
ret |= caa[i] ^ cab[i];
}
return ret == 0;
}
注意 equalsNoEarlyReturn(hashed, hashpw(plaintext, hashed))
這里,第一個參數是加密后的字符串,而第二個參數是用剛才提過的hashpw方法對明文字符串進行加密。
hashpw(plaintext, hashed)
第一個參數是明文,第二個參數是加密字符串,但是在這里是作為鹽值salt傳入的,所以就用到了剛才說的 hashpw 內部通過傳入的salt得到real_salt
,這樣就保證了對現在要校驗的明文的加密和得到已有密文的加密用的是同樣的加密策略,算法和鹽值都相同,這樣如果新產生的密文和原來的密文相同,則這兩個密文對應的明文字符串就是相等的。
這也說明了加密時使用的鹽值被寫在了最終生成的加密字符串中。
三、總結
BCryptPasswordEncoder使用哈希算法+隨機鹽來對字符串加密。因為哈希是一種不可逆算法,所以密碼認證時需要使用相同的算法+鹽值來對待校驗的明文進行加密,然后比較這兩個密文來進行驗證。BCryptPasswordEncoder在加密時通過從傳入的salt
中獲取real_salt
用來加密,保證了這一點。