spring security的BCryptPasswordEncoder加密和對(duì)密碼驗(yàn)證的原理分析
一、加密算法和hash算法
很多項(xiàng)目中有些機(jī)密的信息需要進(jìn)行加密來保護(hù)用戶或者公司的信息安全,這時(shí)這些信息會(huì)采用加密以密文的形式暴露在外面。
加密算法是一種可逆的算法,是通過一定的規(guī)則對(duì)明文進(jìn)行各種計(jì)算的到的密文從而實(shí)現(xiàn)加密的效果。
- hash算法是不可逆的,常見的MD5加密采用的就是hash的算法進(jìn)行加密。加密算法是可逆的,所以很多情況下加密規(guī)則是很重要的,一旦暴露就可以根據(jù)規(guī)則進(jìn)行逆推得到明文,所以加密算法通常會(huì)加鹽,對(duì)稱加密和非對(duì)稱加密就是加鹽的一種。
- hash算法雖然不可逆,但通過大數(shù)據(jù)進(jìn)行匹配很多數(shù)據(jù)可以被找到,所以hash算法也是需要加鹽來保證一定的機(jī)密性。
二、BCryptPasswordEncoder 加密和解密的原理
BCryptPasswordEncoder對(duì)同樣的數(shù)據(jù)比如“11111”進(jìn)行加密,每次加密的結(jié)果是不相同的,此時(shí)就要思考一個(gè)問題,同樣的數(shù)據(jù)每次加密不同,那么它是如何進(jìn)行解密的呢?
下面來分析一下此方式的加密源碼,參考了網(wǎng)上的一些資料:
三、源碼解析
BCryptPasswordEncoder類實(shí)現(xiàn)了PasswordEncoder接口,這個(gè)接口中定義了兩個(gè)方法
public interface PasswordEncoder { String encode(CharSequence rawPassword); boolean matches(CharSequence rawPassword, String encodedPassword); }
其中encode(...)是對(duì)字符串進(jìn)行加密的方法,matches使用來校驗(yàn)傳入的明文密碼rawPassword是否和加密密碼encodedPassword相匹配的方法。
即對(duì)密碼進(jìn)行加密時(shí)調(diào)用encode,登錄認(rèn)證時(shí)調(diào)用matches
下面我們來看下BCryptPasswordEncoder類中這兩個(gè)方法的具體實(shí)現(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); }
可以看到,這個(gè)方法中先基于某種規(guī)則得到了一個(gè)鹽值,然后在調(diào)用BCrypt.hashpw方法,傳入明文密碼和鹽值salt。所以我們?cè)倏聪翨Crypt.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(); }
可以看到,這個(gè)方法中先根據(jù)傳入的鹽值salt
,然后基于某種規(guī)則從salt得到real_salt
,后續(xù)的操作都是用這個(gè)real_salt來進(jìn)行,最終得到加密字符串。
所以這里有一個(gè)重點(diǎn):傳入的鹽值salt
并不是最終用來加密的鹽,方法中通過salt得到了real_salt
,記住這一點(diǎn),因?yàn)楹筮叺钠ヅ浞椒╩atches中要用到這一點(diǎn)。
3. matches方法
matches方法用來判斷一個(gè)明文是否和一個(gè)加密字符串對(duì)應(yīng)。
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); }
這個(gè)方法中先對(duì)密文字符串進(jìn)行了一些校驗(yàn),如果不符合規(guī)則直接返回不匹配,然后調(diào)用校驗(yàn)方法BCrypt.checkpw,第一個(gè)參數(shù)是明文,第二個(gè)參數(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))
這里,第一個(gè)參數(shù)是加密后的字符串,而第二個(gè)參數(shù)是用剛才提過的hashpw方法對(duì)明文字符串進(jìn)行加密。
hashpw(plaintext, hashed)
第一個(gè)參數(shù)是明文,第二個(gè)參數(shù)是加密字符串,但是在這里是作為鹽值salt傳入的,所以就用到了剛才說的 hashpw 內(nèi)部通過傳入的salt得到real_salt
,這樣就保證了對(duì)現(xiàn)在要校驗(yàn)的明文的加密和得到已有密文的加密用的是同樣的加密策略,算法和鹽值都相同,這樣如果新產(chǎn)生的密文和原來的密文相同,則這兩個(gè)密文對(duì)應(yīng)的明文字符串就是相等的。
這也說明了加密時(shí)使用的鹽值被寫在了最終生成的加密字符串中。
總結(jié)
以上為個(gè)人經(jīng)驗(yàn),希望能給大家一個(gè)參考,也希望大家多多支持腳本之家。
相關(guān)文章
java設(shè)計(jì)模式之單例模式的詳解及優(yōu)點(diǎn)
這篇文章主要介紹了java設(shè)計(jì)模式之單例模式的詳解及優(yōu)點(diǎn)的相關(guān)資料,如果一個(gè)類始終只能創(chuàng)建一個(gè)實(shí)例,那么這個(gè)類被稱為單例類,這種設(shè)計(jì)模式被稱為單例模式,需要的朋友可以參考下2017-08-08Springcloud hystrix服務(wù)熔斷和dashboard如何實(shí)現(xiàn)
這篇文章主要介紹了Springcloud hystrix服務(wù)熔斷和dashboard如何實(shí)現(xiàn),文中通過示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友可以參考下2020-12-12Security6.4.2?自定義異常中統(tǒng)一響應(yīng)遇到的問題
本文主要介紹了Security6.4.2?自定義異常中統(tǒng)一響應(yīng)遇到的問題,文中通過示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2025-03-03Java實(shí)現(xiàn)數(shù)組轉(zhuǎn)字符串及字符串轉(zhuǎn)數(shù)組的方法分析
這篇文章主要介紹了Java實(shí)現(xiàn)數(shù)組轉(zhuǎn)字符串及字符串轉(zhuǎn)數(shù)組的方法,結(jié)合實(shí)例形式分析了Java字符串及數(shù)組相關(guān)的分割、遍歷、追加等操作技巧,需要的朋友可以參考下2018-06-06Java中private關(guān)鍵字詳細(xì)用法實(shí)例以及解釋
這篇文章主要給大家介紹了關(guān)于Java中private關(guān)鍵字詳細(xì)用法實(shí)例以及解釋的相關(guān)資料,在Java中private是一種訪問修飾符,它可以用來控制類成員的訪問權(quán)限,文中將用法介紹的非常詳細(xì),需要的朋友可以參考下2024-01-01java基礎(chǔ)之接口組成更新的實(shí)現(xiàn)
本文主要介紹了java基礎(chǔ)之接口組成更新的實(shí)現(xiàn),文中通過示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2022-04-04