MD5 雖然不再適合高安全性場景(如密碼儲存應用 bcrypt/Argon2),但在檔案完整性校驗、非核心業務簽名、快速指紋生成等領域仍廣泛使用。本文從基礎用法進階到工業級「加鹽驗證」完整實作。
MessageDigest 單向雜湊public static String getMD5(String str) {
try {
MessageDigest md = MessageDigest.getInstance("MD5");
md.update(str.getBytes(StandardCharsets.UTF_8));
// 位元組陣列轉 16 進位字串
return bytesToHex(md.digest());
} catch (NoSuchAlgorithmException e) {
throw new RuntimeException("MD5 算法不存在", e);
}
}
private static String bytesToHex(byte[] bytes) {
StringBuilder sb = new StringBuilder();
for (byte b : bytes) {
sb.append(String.format("%02x", b));
}
return sb.toString();
}
缺點:相同輸入產生相同輸出,易受彩虹表攻擊;無鹽值,無法防禦預計算攻擊。
MyMD5Util 加鹽驗證完整實作核心思想:隨機鹽值 + 密碼 → MD5 雜湊,儲存格式為 鹽值(12 bytes) + 雜湊值(16 bytes) 的 16 進位字串。
public class MyMD5Util {
private static final String HEX = "0123456789ABCDEF";
private static final int SALT_LENGTH = 12; // 12 bytes = 96 bits
private static final SecureRandom RANDOM = new SecureRandom();
/** 16 進位字串 → 位元組陣列 */
public static byte[] hexToBytes(String hex) {
byte[] result = new byte[hex.length() / 2];
for (int i = 0; i < result.length; i++) {
int high = HEX.indexOf(hex.charAt(i * 2));
int low = HEX.indexOf(hex.charAt(i * 2 + 1));
result[i] = (byte) ((high << 4) | low);
}
return result;
}
/** 位元組陣列 → 大寫 16 進位字串 */
public static String bytesToHex(byte[] bytes) {
StringBuilder sb = new StringBuilder(bytes.length * 2);
for (byte b : bytes) {
sb.append(HEX.charAt((b >> 4) & 0xF));
sb.append(HEX.charAt(b & 0xF));
}
return sb.toString();
}
/** 產生加密密碼:隨機鹽 + 密碼 → 雜湊,返回「鹽+雜湊」16 進位字串 */
public static String getEncryptedPwd(String password) {
try {
byte[] salt = new byte[SALT_LENGTH];
RANDOM.nextBytes(salt);
MessageDigest md = MessageDigest.getInstance("MD5");
md.update(salt);
md.update(password.getBytes(StandardCharsets.UTF_8));
byte[] digest = md.digest();
// 組合:鹽(前12) + 雜湊(後16)
byte[] combined = new byte[SALT_LENGTH + digest.length];
System.arraycopy(salt, 0, combined, 0, SALT_LENGTH);
System.arraycopy(digest, 0, combined, SALT_LENGTH, digest.length);
return bytesToHex(combined);
} catch (NoSuchAlgorithmException e) {
throw new RuntimeException("MD5 算法不存在", e);
}
}
/** 驗證密碼:從資料庫值提取鹽,重新計算雜湊比對 */
public static boolean validPassword(String password, String dbHex) {
try {
byte[] dbBytes = hexToBytes(dbHex);
byte[] salt = Arrays.copyOfRange(dbBytes, 0, SALT_LENGTH);
byte[] dbDigest = Arrays.copyOfRange(dbBytes, SALT_LENGTH, dbBytes.length);
MessageDigest md = MessageDigest.getInstance("MD5");
md.update(salt);
md.update(password.getBytes(StandardCharsets.UTF_8));
byte[] calcDigest = md.digest();
return MessageDigest.isEqual(calcDigest, dbDigest); // 定時比較,防時序攻擊
} catch (NoSuchAlgorithmException e) {
throw new RuntimeException("MD5 算法不存在", e);
}
}
}
public class Client {
private static final Map<String, String> userDb = new ConcurrentHashMap<>();
public static void main(String[] args) {
register("user1", "123456");
register("user2", "abcdef");
System.out.println(login("user1", "123456") ? "登入成功" : "密碼錯誤");
System.out.println(login("user1", "wrong") ? "登入成功" : "密碼錯誤");
}
static void register(String user, String pwd) {
userDb.put(user, MyMD5Util.getEncryptedPwd(pwd));
}
static boolean login(String user, String pwd) {
String dbPwd = userDb.get(user);
return dbPwd != null && MyMD5Util.validPassword(pwd, dbPwd);
}
}
| 細節 | 做法 | 目的 |
|---|---|---|
| 隨機鹽值 | SecureRandom 產生 12 bytes | 相同密碼產生不同雜湊,防禦彩虹表 |
| 鹽值儲存 | 前置於雜湊值前,同欄位儲存 | 無需額外欄位,驗證時自動提取 |
| 定時比較 | MessageDigest.isEqual() | 防止時序側通道攻擊 |
| 編碼指定 | StandardCharsets.UTF_8 | 跨平台一致性,避免預設編碼差異 |
| 異常處理 | 包裝為 RuntimeException | 簡化呼叫端,算法不存在屬系統級錯誤 |
BCrypt.hashpw(),內建鹽值、可調成本因子)SecretKeyFactory.getInstance("PBKDF2WithHmacSHA256"))掌握「鹽值生成 → 混合運算 → 組合儲存 → 提取驗證」這一完整流程,是理解現代密碼學工程實踐的基石。