spring Security中的BCryptPasswordEncoder類采用SHA-256 +隨機(jī)鹽+密鑰對密碼進(jìn)行加密。SHA是一系列的加密算法,有SHA-1、SHA-2定续、SHA-3三大類,SHA-256是SHA-2下細(xì)分出的一種算法纵揍,此算法發(fā)生哈希碰撞的概率幾乎為0,安全性高议街。
- BCryptPasswordEncoder類實(shí)現(xiàn)了PasswordEncoder接口的encode和matches方法泽谨,來進(jìn)行密碼加密和匹配
- 加密(encode):注冊用戶時(shí),使用SHA-256+隨機(jī)鹽+密鑰把用戶輸入的密碼進(jìn)行hash處理傍睹,得到密碼的hash值隔盛,然后將其存入數(shù)據(jù)庫中。
- BCryptPasswordEncoder類定義了兩個(gè)final變量拾稳,用來控制encode方法的加密規(guī)則吮炕。strength是一個(gè)取值在-1或者4~31之間的int變量,而繼承了java.util.random的SecureRandom類則提供了一種強(qiáng)加密RNG手段(PRNG)访得,random是一個(gè)SecureRandom類的final變量龙亲,為后續(xù)生成salt起作用陕凹。
private final int strength; private final SecureRandom random;
- encode方法根據(jù)strength值的不同和有無SecureRandom對象使用了三種方式生成salt,但這三種方式本質(zhì)其實(shí)是類似的,底層都是調(diào)用BCrypt類的gensalt(this.strength, this.random)方法鳄炉,只是如果沒有傳入自定義的strength和SecureRandom對象杜耙,BCrypt類會自動幫我們將strength設(shè)為10和實(shí)例化SecureRandom對象傳入方法中:
public String encode(CharSequence rawPassword) { //聲明一個(gè)“鹽”變量 String salt; //生成隨機(jī)鹽 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); }
- 只有當(dāng)strength在[4,31]取值時(shí),gensalt方法才會返回“鹽”值拂盯,此方法通過調(diào)用random.nextBytes()和encode_base64()方法編碼生成隨機(jī)鹽字符串;nextBytes()方法會調(diào)用SecureRandomSpi抽象類的engineNextBytes方法生成一串長度為16隨機(jī)的byte數(shù)組佑女,而encode_base64()方法通過多次借助byte數(shù)組和長度為64的char數(shù)組base64_code(包含大部分ASCII字符)進(jìn)行Base64編碼,最終生成長度為29的隨機(jī)鹽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()方法進(jìn)行加密团驱,該方法對傳入的鹽進(jìn)行了一系列校驗(yàn)(長度、版本等等)空凸,確保是有效salt嚎花。同時(shí)將原始密碼轉(zhuǎn)成passwordb字節(jié)數(shù)組。一個(gè)有效的salt前7位是校驗(yàn)位呀洲,包含了鹽版本紊选、鹽rounds,第8位到30位為real_salt道逗,將real_salt傳入decode_base64方法進(jìn)行轉(zhuǎn)碼兵罢,字符串real_salt被轉(zhuǎn)換成字節(jié)數(shù)組輸出給長度為16的saltb,接著將saltb和passwordb字節(jié)數(shù)組傳入crypt_raw()方法進(jìn)行SHA-256加密生成偽隨機(jī)hash值憔辫,最后將saltb和hash值分別進(jìn)行encode_base64方法進(jìn)行Base64編碼(其中saltb字節(jié)數(shù)組通過編碼重新變成realSalt字符串,這也是后續(xù)matches方法匹配密碼的
)趣些,產(chǎn)生的結(jié)果拼接成60位的隨機(jī)密碼仿荆,前7位同樣是校驗(yàn)位贰您,第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):用戶登錄時(shí)拢操,密碼匹配階段并沒有進(jìn)行密碼解密(因?yàn)槊艽a經(jīng)過Hash處理锦亦,是不可逆的),而是將輸入的密碼與數(shù)據(jù)庫查出的密碼同樣傳入BCrypt類的pwhash()中進(jìn)行加密令境,由于算法將加密后密碼的第8位到30位作為real_salt,第一次執(zhí)行pwhash方法傳入的鹽和第二次傳入的鹽值(數(shù)據(jù)庫密碼)是包含關(guān)系杠园,兩者的前30位是相同的。那么根據(jù)相同的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):注冊用戶時(shí),使用SHA-256+隨機(jī)鹽+密鑰把用戶輸入的密碼進(jìn)行hash處理傍睹,得到密碼的hash值隔盛,然后將其存入數(shù)據(jù)庫中。