一般記錄用戶密碼,我們都是通過MD5加密配置的形式。這裏記錄一下,MD5加密的工具類。
package com.mms.utils;
import java.security.MessageDigest;
/**
* Created by codermen on 2017/10/26.
*/
public class MD5Util {
public static void main(String[] args) {
String pwd = getMD5("99991");
System.out.println(pwd);
}
//生成MD5
public static String getMD5(String message) {
String md5 = "";
try {
MessageDigest md = MessageDigest.getInstance("MD5"); // 創建一個md5算法對象
byte[] messageByte = message.getBytes("UTF-8");
byte[] md5Byte = md.digest(messageByte); // 獲得MD5字節數組,16*8=128位
md5 = bytesToHex(md5Byte); // 轉換爲16進制字符串
} catch (Exception e) {
e.printStackTrace();
}
return md5;
}
// 二進制轉十六進制
public static String bytesToHex(byte[] bytes) {
StringBuffer hexStr = new StringBuffer();
int num;
for (int i = 0; i < bytes.length; i++) {
num = bytes[i];
if(num < 0) {
num += 256;
}
if(num < 16){
hexStr.append("0");
}
hexStr.append(Integer.toHexString(num));
}
return hexStr.toString().toUpperCase();
}
}
淺談使用springsecurity中的BCryptPasswordEncoder方法對密碼進行加密(encode)與密碼匹配(matches)
spring security中的BCryptPasswordEncoder方法採用SHA-256 +隨機鹽+密鑰對密碼進行加密。SHA系列是Hash算法,不是加密算法,使用加密算法意味着可以解密(這個與編碼/解碼一樣),但是採用Hash處理,其過程是不可逆的。
(1)加密(encode):註冊用戶時,使用SHA-256+隨機鹽+密鑰把用戶輸入的密碼進行hash處理,得到密碼的hash值,然後將其存入數據庫中。
(2)密碼匹配(matches):用戶登錄時,密碼匹配階段並沒有進行密碼解密(因爲密碼經過Hash處理,是不可逆的),而是使用相同的算法把用戶輸入的密碼進行hash處理,得到密碼的hash值,然後將其與從數據庫中查詢到的密碼hash值進行比較。如果兩者相同,說明用戶輸入的密碼正確。
這正是爲什麼處理密碼時要用hash算法,而不用加密算法。因爲這樣處理即使數據庫泄漏,黑客也很難破解密碼(破解密碼只能用彩虹表)。
學習到這一塊,查看了一些源碼。以BCryptPasswordEncoder爲例
public class BCryptPasswordEncoderTest {
public static void main(String[] args) {
String pass = "admin";
BCryptPasswordEncoder bcryptPasswordEncoder = new BCryptPasswordEncoder();
String hashPass = bcryptPasswordEncoder.encode(pass);
System.out.println(hashPass);
boolean f = bcryptPasswordEncoder.matches("admin",hashPass);
System.out.println(f);
}
}
可以看到,每次輸出的hashPass 都不一樣,但是最終的f都爲 true,即匹配成功。查看代碼,可以看到,其實每次的隨機鹽,都保存在hashPass中。在進行matchs進行比較時,調用BCrypt 的String hashpw(String password, String salt)方法。兩個參數即”admin“和 hashPass
//******BCrypt.java******salt即取出要比較的DB中的密碼*******
real_salt = salt.substring(off + 3, off + 25);
try {
// ***************************************************
passwordb = (password + (minor >= 'a' ? "\000" : "")).getBytes("UTF-8");
}
catch (UnsupportedEncodingException uee) {}
saltb = decode_base64(real_salt, BCRYPT_SALT_LEN);
B = new BCrypt();
hashed = B.crypt_raw(passwordb, saltb, rounds);
假定一次hashPass爲:10$AxafsyVqK51p.s9WAEYWYeIY9TKEoG83LTEOSB3KUkoLtGsBKhCwe隨機鹽即爲 AxafsyVqK51p.s9WAEYWYe(salt = BCrypt.gensalt();中有描述)
可見,隨機鹽(AxafsyVqK51p.s9WAEYWYe),會在比較的時候,重新被取出。即,加密的hashPass中,前部分已經包含了鹽信息。
如果只是想使用SpringSecurity + SpringBoot完成密碼加密/解密操作,而不使用SpringSecurty提供的其它權證驗證功能。具體步驟如下:
1 BCrypt密碼加密
1.1 準備工作
任何應用考慮到安全,絕不能明文的方式保存密碼。密碼應該通過哈希算法進行加密。
有很多標準的算法比如SHA或者MD5,結合salt(鹽)是一個不錯的選擇。 Spring Security
提供了BCryptPasswordEncoder類,實現Spring的PasswordEncoder接口使用BCrypt強
哈希方法來加密密碼。
BCrypt強哈希方法 每次加密的結果都不一樣。
(1)tensquare_user工程的pom引入依賴
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring‐boot‐starter‐security</artifactId>
</dependency>
(2)添加配置類 (資源/工具類中提供)
我們在添加了spring security依賴後,所有的地址都被spring security所控制了,我們目
前只是需要用到BCrypt密碼加密的部分,所以我們要添加一個配置類,配置爲所有地址
都可以匿名訪問
/**
* 安全配置類
*/
@Configuration
@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter{
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.authorizeRequests()
.antMatchers("/**").permitAll()
.anyRequest().authenticated()
.and().csrf().disable();
}
}
(3)修改tensquare_user工程的Application, 配置bean
@Bean
public BCryptPasswordEncoder bcryptPasswordEncoder(){
return new BCryptPasswordEncoder();
}
之後就可以使用BCryptPasswordEncoder中的方法完成加密/解密操作:
加密:
bcryptPasswordEncoder.encoder(password)
解密:
bcrytPasswordEncoder.matches(rawPassword,encodedPassword)
在Spring Security下 PasswordEncoder 的實現類包含:
PasswordEncoder 使用
首先我們先來看看一個創建密碼編碼器工廠方法
org/springframework/security/crypto/factory/PasswordEncoderFactories.java
public static PasswordEncoder createDelegatingPasswordEncoder() {
String encodingId = "bcrypt";
Map<String, PasswordEncoder> encoders = new HashMap<>();
encoders.put(encodingId, new BCryptPasswordEncoder());
encoders.put("ldap", new org.springframework.security.crypto.password.LdapShaPasswordEncoder());
encoders.put("MD4", new org.springframework.security.crypto.password.Md4PasswordEncoder());
encoders.put("MD5", new org.springframework.security.crypto.password.MessageDigestPasswordEncoder("MD5"));
encoders.put("noop", org.springframework.security.crypto.password.NoOpPasswordEncoder.getInstance());
encoders.put("pbkdf2", new Pbkdf2PasswordEncoder());
encoders.put("scrypt", new SCryptPasswordEncoder());
encoders.put("SHA-1", new org.springframework.security.crypto.password.MessageDigestPasswordEncoder("SHA-1"));
encoders.put("SHA-256", new org.springframework.security.crypto.password.MessageDigestPasswordEncoder("SHA-256"));
encoders.put("sha256", new org.springframework.security.crypto.password.StandardPasswordEncoder());
return new DelegatingPasswordEncoder(encodingId, encoders);
}
上述代碼 encoders 的 Map 包含了很多種密碼編碼器,有 ldap 、MD4 、 MD5 、noop 、pbkdf2 、scrypt 、SHA-1 、SHA-256
上面靜態工廠方法可以看出,默認是創建並返回一個 BCryptPasswordEncoder,同時該 BCryptPasswordEncoder( PasswordEncoder 子類)也是 Spring Security 推薦的默認密碼編碼器,其中 noop 就是不做處理默認保存原密碼。
一般我們代碼中 @Autowired 注入並使用 PasswordEncoder 接口的實例,然後調用其 matches 方法去匹配原密碼和數據庫中保存的“密碼”;密碼的校驗方式有多種,從 PasswordEncoder 接口實現的類是可以知道。
業務代碼中注入 PasswordEncoder
@Autowired
private PasswordEncoder passwordEncoder;
知識混淆點
加密/解密 與 Hash 這兩個概念不能混淆,比如:SHA 系列是 Hash 算法,不是加密算法,加密意味着可以解密,但是 Hash 是不可逆的(無法通過 Hash 值還原得到密碼,只能比對 Hash 值看看是否相等)。
安全性問題
目前很大一部分存在安全問題的系統一般僅僅使用密碼的 MD5 值進行保存,可以通過 MD5 查詢庫去匹配對大部分的密碼(可以直接從彩虹表裏反推出來),而且 MD5 計算 Hash 值碰撞容易構造,安全性大大降低。MD5 加鹽在本地計算速度也是很快,也是密碼短也是極其容易破解;更好的選擇是 SHA-256、BCrypt 等等等
密碼匹配流程的源碼解釋
本文簡單說一下 BCryptPasswordEncoder 密碼匹配的一個簡單流程或者過程。
重點
如果是使用 BCryptPasswordEncoder 調用 encode() 方法編碼輸入密碼的話,其實這個編碼後的“密碼”並不是我們平時輸入的真正密碼,而是密碼加鹽後的通過單向 Hash 算法(BCrypt)得到值。
這裏面細心的同學可能會發現一些問題:
-
同一個密碼計算 Hash 不應該是一樣的嗎?每次使用 BCryptPasswordEncoder 編碼同一個密碼都是不一樣的?
-
BCryptPasswordEncoder 編碼同一個密碼後結果都不一樣,怎麼進行匹配?
下面通過源碼簡單說一下這個匹配的流程:
matches(CharSequence rawPassword, String encodedPassword)
方法根據兩個參數都可以知道
- 第一個參數是原密碼
- 第二個參數就是用
PasswordEncoder
調用encode(CharSequence rawPassword)
編碼過後保存在數據庫的密碼。
org/springframework/security/crypto/bcrypt/BCryptPasswordEncoder.java
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);
}
上述代碼解讀:首先判斷是否數據庫保存的“密碼”(後面簡稱:“密碼”)是否爲空或者 null
,在通過正則表達式匹配“密碼”是否符合格式,最後通過 BCrypt
的 checkpw(String plaintext, String hashed)
方法進行密碼匹配。
再詳細看看 BCrypt
的 checkpw(String plaintext, String hashed)
方法:
org/springframework/security/crypto/bcrypt/BCrypt.java
public static boolean checkpw(String plaintext, String hashed) {
return equalsNoEarlyReturn(hashed, hashpw(plaintext, hashed));
}
第二個參數 hashed
表明其實數據庫查詢出來的“密碼”也就是 Hash 值;equalsNoEarlyReturn(hashed, hashpw(plaintext, hashed))
代碼中通過調用 hashpw
計算輸入密碼的 Hash 值(參數分別是輸入的密碼和保存在數據庫的“密碼”)
再繼續看 hashpw
裏面的部分代碼(內容過長,省略部分代碼,看看代碼中的中文註釋):
org/springframework/security/crypto/bcrypt/BCrypt.java
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就是 Hash 計算前的密碼鹽(關於鹽的介紹:https://zh.wikipedia.org/wiki/%E7%9B%90_(%E5%AF%86%E7%A0%81%E5%AD%A6))
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 )進行一系列校驗(長度校驗等)並截取“密碼”中相應的密碼鹽,利用這個密碼鹽進行同樣的一系列計算 Hash 操作和 Base64 編碼拼接一些標識符 生成所謂的“密碼”,最後 equalsNoEarlyReturn
方法對同一個密碼鹽生成的兩個“密碼”進行匹配。
上述大致就是密碼匹配流程了,對於問題“ BCryptPasswordEncoder 編碼同一個密碼後結果都不一樣,怎麼進行匹配”
的簡單解答:
因爲密碼鹽是隨機生成的,但是可以根據數據庫查詢出來的“密碼”拿到密碼鹽,同一個密碼鹽+原密碼計算 Hash 結果值是能匹配的。
密碼“加密”保存源碼解釋
看看加密的一個過程,
org/springframework/security/crypto/bcrypt/BCryptPasswordEncoder.java
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);
}
encode 方法傳入是原密碼,其中 int strength, SecureRandom random
這兩個構造參數是 BCryptPasswordEncoder(int strength, SecureRandom random)
構造方法按需傳入,如果不指定strength和random,默認執行 BCrypt.gensalt()
這行代碼生成也相應密碼隨機鹽。
先看看 gensalt(int log_rounds, SecureRandom random)
方法的代碼(可以看看中文註釋):
org/springframework/security/crypto/bcrypt/BCrypt.java
public static String gensalt(int log_rounds, SecureRandom random) {
// 一些檢驗
if (log_rounds < MIN_LOG_ROUNDS || log_rounds > MAX_LOG_ROUNDS) {
throw new IllegalArgumentException("Bad number of rounds");
}
StringBuilder rs = new StringBuilder();
byte rnd[] = new byte[BCRYPT_SALT_LEN];
// 生成隨機字節並將其置於rnd字節數組
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();
}
最終上面的 gensalt
方法得到一個 隨機密碼鹽+無用字符串(這個字符串可以理解爲你輸入的密碼) 計算 Hash 操作和 Base64 編碼拼接一些標識符 生成假“密碼”(這個假“密碼”爲了兼容方便調用 hashpw
方法),最後關鍵點就是調用 BCrypt.hashpw
方法取到密碼鹽生成相應的真實“密碼”(這個得到的密碼可以用於保存在數據庫中了)。
對於問題“同一個密碼計算 Hash 不應該是一樣的嗎?每次使用 BCryptPasswordEncoder 編碼同一個密碼都是不一樣的?”
的簡單解答:
因爲用到的隨機密碼鹽每次都是不一樣的,同一個密碼和不同的密碼鹽組合計算出來的 Hash 值肯定不一樣啦,所以編碼同一個密碼得到的結果都是不一樣。
建議和想法
本文主要講解一些安全性防護的思想,學習的過程思想很重要。
登錄註冊是每個系統都具備的功能,開發的同學記住一定不能保存明文密碼,否則被脫庫就會造成嚴重的後果。如果是通過上述的方法進行密碼保存,即便拿到“密碼”也非常難還原密碼。
上述在密碼編碼的過程中的思想還是需要掌握:
-
只是保存散列碼是不安全的,但是我們可以爲密碼加鹽再通過一些 Hash 值 低概率碰撞且計算速度慢 的散列算法計算 Hash 值保存。
-
Spring Security 每次 Hash 之前用的鹽都是隨機,鹽可以保存在最終生成的“密碼”中,這樣每個密碼都是用了相應不同的隨機鹽+原密碼計算 Hash 值得到,暴力破解難度也變大了。
https://www.jianshu.com/p/922963106729