MD5 是 Message Digest Algorithm 的縮寫,譯為資訊摘要演演算法,它是 Java 語言中使用很廣泛的一種加密演演算法。MD5 可以將任意字串,通過不可逆的字串變換演演算法,生成一個唯一的 MD5 資訊摘要,這個資訊摘要也就是我們通常所說的 MD5 字串。那麼問題來了,MD5 加密安全嗎?
這道題看似簡單,其實是一道送命題,很多人尤其是一些新入門的同學會覺得,安全啊,MD5 首先是加密的字串,其次是不可逆的,所以它一定是安全的。如果你這樣回答,那麼就徹底掉進面試官給你挖好的坑了。
為什麼呢?因為答案是「不安全」,而不是「安全」。
MD5 之所以說它是不安全的,是因為每一個原始密碼都會生成一個對應的固定密碼,也就是說一個字串生成的 MD5 值是永遠不變的。這樣的話,雖然它是不可逆的,但可以被窮舉,而窮舉的「產品」就叫做彩虹表。
彩虹表是一個用於加密雜湊函數逆運算的預先計算好的表, 為破解密碼的雜湊值(或稱雜湊值、微縮圖、摘要、指紋、雜湊密文)而準備。 一般主流的彩虹表都在 100G 以上。這樣的表常常用於恢復由有限集字元組成的固定長度的純文字密碼。這是空間/時間替換的典型實踐,比每一次嘗試都計算雜湊的暴力破解處理時間少而儲存空間多,但卻比簡單的對每條輸入雜湊翻查表的破解方式儲存空間少而處理時間多。
簡單來說,彩虹表就是一個很大的,用於存放窮舉對應值的資料表。 以 MD5 為例,「1」的 MD5 值是「C4CA4238A0B923820DCC509A6F75849B」,而「2」的 MD5 值是「C81E728D9D4C2F636F067F89CC14862C」,那麼就會有一個 MD5 的彩虹表是這樣的:
原始值 | 加密值 |
---|---|
1 | C4CA4238A0B923820DCC509A6F75849B |
2 | C81E728D9D4C2F636F067F89CC14862C |
... | ... |
大家想想,如果有了這張表之後,那麼我就可以通過 MD5 的密文直接查到原始密碼了,所以說資料庫如果只使用 MD5 加密,這就好比用了一把插了鑰匙的鎖一樣不安全。
想要解決以上問題,我們需要引入「加鹽」機制。
鹽(Salt):在密碼學中,是指通過在密碼任意固定位置插入特定的字串,讓雜湊後的結果和使用原始密碼的雜湊結果不相符,這種過程稱之為「加鹽」。
說的通俗一點「加鹽」就像炒菜一樣,放不同的鹽,炒出菜的味道就是不同的,咱們之前使用 MD5 不安全的原因是,每個原始密碼所對應的 MD5 值都是固定的,那我們只需要讓密碼每次通過加鹽之後,生成的最終密碼都不同,這樣就能解決加密不安全的問題了。
加鹽是一種手段、是一種解決密碼安全問題的思路,而它的實現手段有很多種,我們可以使用框架如 Spring Security 提供的 BCrypt 進行加鹽和驗證,當然,我們也可以自己實現加鹽的功能。
本文為了讓大家更好的理解加鹽的機制,所以我們自己來動手來實現一下加鹽的功能。
實現加鹽機制的關鍵是在加密的過程中,生成一個隨機的鹽值,而且隨機鹽值儘量不要重複,這時,我們就可以使用 Java 語言提供的 UUID(Universally Unique Identifier,通用唯一識別碼)來作為鹽值,這樣每次都會生成一個不同的隨機鹽值,且永不重複。
加鹽的實現程式碼如下:
import org.springframework.util.DigestUtils;
import org.springframework.util.StringUtils;
import java.util.UUID;
public class PasswordUtil {
/**
* 加密(加鹽處理)
* @param password 待加密密碼(需要加密的密碼)
* @return 加密後的密碼
*/
public static String encrypt(String password) {
// 隨機鹽值 UUID
String salt = UUID.randomUUID().toString().replaceAll("-", "");
// 密碼=md5(隨機鹽值+密碼)
String finalPassword = DigestUtils.md5DigestAsHex((salt + password).getBytes());
return salt + "$" + finalPassword;
}
}
從上述程式碼我們可以看出,加鹽的實現具體步驟是:
那麼,問題來了,既然每次生成的密碼都不同,那麼怎麼驗證密碼是否正確呢?
要驗證密碼是否正確的關鍵是需要先獲取鹽值,然後再使用相同的加密方式和步驟,生成一個最終密碼和和資料庫中儲存的加密密碼進行對比,具體實現程式碼如下:
import org.springframework.util.DigestUtils;
import org.springframework.util.StringUtils;
import java.util.UUID;
public class PasswordUtil {
/**
* 加密(加鹽處理)
* @param password 待加密密碼(需要加密的密碼)
* @return 加密後的密碼
*/
public static String encrypt(String password) {
// 隨機鹽值 UUID
String salt = UUID.randomUUID().toString().replaceAll("-", "");
// 密碼=md5(隨機鹽值+密碼)
String finalPassword = DigestUtils.md5DigestAsHex((salt + password).getBytes());
return salt + "$" + finalPassword;
}
/**
* 解密
* @param password 要驗證的密碼(未加密)
* @param securePassword 資料庫中的加了鹽值的密碼
* @return 對比結果 true OR false
*/
public static boolean decrypt(String password, String securePassword) {
boolean result = false;
if (StringUtils.hasLength(password) && StringUtils.hasLength(securePassword)) {
if (securePassword.length() == 65 && securePassword.contains("$")) {
String[] securePasswordArr = securePassword.split("\\$");
// 鹽值
String slat = securePasswordArr[0];
String finalPassword = securePasswordArr[1];
// 使用同樣的加密演演算法和隨機鹽值生成最終加密的密碼
password = DigestUtils.md5DigestAsHex((slat + password).getBytes());
if (finalPassword.equals(password)) {
result = true;
}
}
}
return result;
}
}
只是簡單的使用 MD5 加密是不安全的,因為每個字串都會生成固定的密文,那麼我們就可以使用彩虹表將密文還原出來,所以它不是安全的。想要解決這個問題,我們需要通過加鹽的手段,每次生成一個不同的密碼,就把這個問題解決了。
是非審之於己,譭譽聽之於人,得失安之於數。
公眾號:Java面試真題解析