spring Security中的BCryptPasswordEncoder類采用SHA-256 +隨機鹽+密鑰對密碼進行加密。SHA是一系列的加密算法,有SHA-1、SHA-2、SHA-3三大類,SHA-256是SHA-2下細分出的一種算法,此算法發生哈希碰撞的概率幾乎為0,安全性高。
- BCryptPasswordEncoder類實現了PasswordEncoder接口的encode和matches方法,來進行密碼加密和匹配
- 加密(encode):注冊用戶時,使用SHA-256+隨機鹽+密鑰把用戶輸入的密碼進行hash處理,得到密碼的hash值,然后將其存入數據庫中。
- BCryptPasswordEncoder類定義了兩個final變量,用來控制encode方法的加密規則。strength是一個取值在-1或者4~31之間的int變量,而繼承了java.util.random的SecureRandom類則提供了一種強加密RNG手段(PRNG),random是一個SecureRandom類的final變量,為后續生成salt起作用。
private final int strength; private final SecureRandom random;
- encode方法根據strength值的不同和有無SecureRandom對象使用了三種方式生成salt,但這三種方式本質其實是類似的,底層都是調用BCrypt類的gensalt(this.strength, this.random)方法,只是如果沒有傳入自定義的strength和SecureRandom對象,BCrypt類會自動幫我們將strength設為10和實例化SecureRandom對象傳入方法中:
public String encode(CharSequence rawPassword) { //聲明一個“鹽”變量 String salt; //生成隨機鹽 if (this.strength > 0) { if (this.random != null) { salt = BCrypt.gensalt(this.strength, this.random); } else { salt = BCrypt.gensalt(this.strength); } } else { salt = BCrypt.gensalt(); } return BCrypt.hashpw(rawPassword.toString(), salt); }
- 只有當strength在[4,31]取值時,gensalt方法才會返回“鹽”值,此方法通過調用random.nextBytes()和encode_base64()方法編碼生成隨機鹽字符串;nextBytes()方法會調用SecureRandomSpi抽象類的engineNextBytes方法生成一串長度為16隨機的byte數組,而encode_base64()方法通過多次借助byte數組和長度為64的char數組base64_code(包含大部分ASCII字符)進行Base64編碼,最終生成長度為29的隨機鹽salt字符串。
public static String gensalt(int log_rounds, SecureRandom random) { if (log_rounds >= 4 && log_rounds <= 31) { StringBuilder rs = new StringBuilder(); byte[] rnd = new byte[16]; random.nextBytes(rnd); rs.append("$2a$"); if (log_rounds < 10) { rs.append("0"); } rs.append(log_rounds); rs.append("$"); encode_base64(rnd, rnd.length, rs); return rs.toString(); } else { throw new IllegalArgumentException("Bad number of rounds"); } } static void encode_base64(byte[] d, int len, StringBuilder rs) throws IllegalArgumentException { int off = 0; if (len > 0 && len <= d.length) { while(off < len) { int c1 = d[off++] & 255; rs.append(base64_code[c1 >> 2 & 63]); c1 = (c1 & 3) << 4; if (off >= len) { rs.append(base64_code[c1 & 63]); break; } int c2 = d[off++] & 255; c1 |= c2 >> 4 & 15; rs.append(base64_code[c1 & 63]); c1 = (c2 & 15) << 2; if (off >= len) { rs.append(base64_code[c1 & 63]); break; } c2 = d[off++] & 255; c1 |= c2 >> 6 & 3; rs.append(base64_code[c1 & 63]); rs.append(base64_code[c2 & 63]); } } else { throw new IllegalArgumentException("Invalid len"); } }
- 將生成的鹽值和原始密碼傳入BCrypt類的hashpw()方法進行加密,該方法對傳入的鹽進行了一系列校驗(長度、版本等等),確保是有效salt。同時將原始密碼轉成passwordb字節數組。一個有效的salt前7位是校驗位,包含了鹽版本、鹽rounds,第8位到30位為real_salt,將real_salt傳入decode_base64方法進行轉碼,字符串real_salt被轉換成字節數組輸出給長度為16的saltb,接著將saltb和passwordb字節數組傳入crypt_raw()方法進行SHA-256加密生成偽隨機hash值,最后將saltb和hash值分別進行encode_base64方法進行Base64編碼(其中saltb字節數組通過編碼重新變成realSalt字符串,這也是后續matches方法匹配密碼的
),產生的結果拼接成60位的隨機密碼,前7位同樣是校驗位,第8位到30位為real_salt。
public static String hashpw(String password, String salt) throws IllegalArgumentException { .....dosomework..... int rounds = Integer.parseInt(salt.substring(off, off + 2)); String real_salt = salt.substring(off + 3, off + 25); byte[] passwordb; try { passwordb = (password + (minor >= 'a' ? "\u0000" : "")).getBytes("UTF-8"); } catch (UnsupportedEncodingException var13) { throw new AssertionError("UTF-8 is not supported"); } byte[] saltb = decode_base64(real_salt, 16); BCrypt B = new BCrypt(); byte[] 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(); }
- 密碼匹配(matches):用戶登錄時,密碼匹配階段并沒有進行密碼解密(因為密碼經過Hash處理,是不可逆的),而是將輸入的密碼與數據庫查出的密碼同樣傳入BCrypt類的pwhash()中進行加密,由于算法將加密后密碼的第8位到30位作為real_salt,第一次執行pwhash方法傳入的鹽和第二次傳入的鹽值(數據庫密碼)是包含關系,兩者的前30位是相同的。那么根據相同的real_salt和相同的password生成的加密密碼很顯然也是相同的。
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; } }
- 加密(encode):注冊用戶時,使用SHA-256+隨機鹽+密鑰把用戶輸入的密碼進行hash處理,得到密碼的hash值,然后將其存入數據庫中。