spring security的BCryptPasswordEncoder加密和對密碼驗證的原理分析
一、加密算法和hash算法
很多項目中有些機密的信息需要進行加密來保護用戶或者公司的信息安全,這時這些信息會采用加密以密文的形式暴露在外面。
加密算法是一種可逆的算法,是通過一定的規(guī)則對明文進行各種計算的到的密文從而實現(xiàn)加密的效果。
- hash算法是不可逆的,常見的MD5加密采用的就是hash的算法進行加密。加密算法是可逆的,所以很多情況下加密規(guī)則是很重要的,一旦暴露就可以根據(jù)規(guī)則進行逆推得到明文,所以加密算法通常會加鹽,對稱加密和非對稱加密就是加鹽的一種。
- hash算法雖然不可逆,但通過大數(shù)據(jù)進行匹配很多數(shù)據(jù)可以被找到,所以hash算法也是需要加鹽來保證一定的機密性。
二、BCryptPasswordEncoder 加密和解密的原理
BCryptPasswordEncoder對同樣的數(shù)據(jù)比如“11111”進行加密,每次加密的結果是不相同的,此時就要思考一個問題,同樣的數(shù)據(jù)每次加密不同,那么它是如何進行解密的呢?
下面來分析一下此方式的加密源碼,參考了網上的一些資料:
三、源碼解析
BCryptPasswordEncoder類實現(xiàn)了PasswordEncoder接口,這個接口中定義了兩個方法
public interface PasswordEncoder {
String encode(CharSequence rawPassword);
boolean matches(CharSequence rawPassword, String encodedPassword);
}其中encode(...)是對字符串進行加密的方法,matches使用來校驗傳入的明文密碼rawPassword是否和加密密碼encodedPassword相匹配的方法。
即對密碼進行加密時調用encode,登錄認證時調用matches
下面我們來看下BCryptPasswordEncoder類中這兩個方法的具體實現(xiàn)
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);
}可以看到,這個方法中先基于某種規(guī)則得到了一個鹽值,然后在調用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();
}可以看到,這個方法中先根據(jù)傳入的鹽值salt,然后基于某種規(guī)則從salt得到real_salt,后續(xù)的操作都是用這個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);
}這個方法中先對密文字符串進行了一些校驗,如果不符合規(guī)則直接返回不匹配,然后調用校驗方法BCrypt.checkpw,第一個參數(shù)是明文,第二個參數(shù)是加密后的字符串。
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))
這里,第一個參數(shù)是加密后的字符串,而第二個參數(shù)是用剛才提過的hashpw方法對明文字符串進行加密。
hashpw(plaintext, hashed)
第一個參數(shù)是明文,第二個參數(shù)是加密字符串,但是在這里是作為鹽值salt傳入的,所以就用到了剛才說的 hashpw 內部通過傳入的salt得到real_salt,這樣就保證了對現(xiàn)在要校驗的明文的加密和得到已有密文的加密用的是同樣的加密策略,算法和鹽值都相同,這樣如果新產生的密文和原來的密文相同,則這兩個密文對應的明文字符串就是相等的。
這也說明了加密時使用的鹽值被寫在了最終生成的加密字符串中。
總結
以上為個人經驗,希望能給大家一個參考,也希望大家多多支持腳本之家。
相關文章
Springcloud hystrix服務熔斷和dashboard如何實現(xiàn)
這篇文章主要介紹了Springcloud hystrix服務熔斷和dashboard如何實現(xiàn),文中通過示例代碼介紹的非常詳細,對大家的學習或者工作具有一定的參考學習價值,需要的朋友可以參考下2020-12-12
Security6.4.2?自定義異常中統(tǒng)一響應遇到的問題
本文主要介紹了Security6.4.2?自定義異常中統(tǒng)一響應遇到的問題,文中通過示例代碼介紹的非常詳細,對大家的學習或者工作具有一定的參考學習價值,需要的朋友們下面隨著小編來一起學習學習吧2025-03-03
Java實現(xiàn)數(shù)組轉字符串及字符串轉數(shù)組的方法分析
這篇文章主要介紹了Java實現(xiàn)數(shù)組轉字符串及字符串轉數(shù)組的方法,結合實例形式分析了Java字符串及數(shù)組相關的分割、遍歷、追加等操作技巧,需要的朋友可以參考下2018-06-06

