MD5 基礎認知
- 本質:訊息摘要算法,生成 128-bit (16 字節) 指紋
- 不可逆:單向函數,無法從摘要還原原文
- 碰撞風險:已被證明存在碰撞,**不適合安全敏感場景** (密碼存儲請用 BCrypt/Argon2)
- 適用場景:文件完整性校驗、緩存 Key 生成、非敏感數據指紋
標準 32 位實現
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
public class MD5Utils {
private static final char[] HEX_DIGITS =
{'0','1','2','3','4','5','6','7','8','9','a','b','c','d','e','f'};
/**
* 計算 MD5,返回 32 位十六進制字符串
*/
public static String md5(String input) {
return md5(input.getBytes(StandardCharsets.UTF_8));
}
public static String md5(byte[] input) {
try {
MessageDigest md = MessageDigest.getInstance("MD5");
byte[] digest = md.digest(input);
return bytesToHex(digest);
} catch (NoSuchAlgorithmException e) {
throw new IllegalStateException("MD5 算法不可用", e);
}
}
/**
* 字節數組轉十六進制 (高效無對象創建版本)
*/
private static String bytesToHex(byte[] bytes) {
char[] chars = new char[bytes.length * 2];
for (int i = 0; i < bytes.length; i++) {
int b = bytes[i] & 0xFF;
chars[i * 2] = HEX_DIGITS[b >>> 4];
chars[i * 2 + 1] = HEX_DIGITS[b & 0x0F];
}
return new String(chars);
}
/**
* 16 位 MD5:取 32 位中間 16 位 (第 8-23 字符)
*/
public static String md5_16(String input) {
String md5_32 = md5(input);
return md5_32.substring(8, 24);
}
}
進階:帶鹽值的 MD5 (防彩虹表)
public class SaltedMD5 {
private static final SecureRandom RANDOM = new SecureRandom();
private static final int SALT_LENGTH = 16; // 16 字節鹽值
/**
* 生成鹽值 + 雜湊組合字符串: salt:hash
*/
public static String generate(String input) {
byte[] salt = new byte[SALT_LENGTH];
RANDOM.nextBytes(salt);
String hash = md5(concat(salt, input.getBytes(StandardCharsets.UTF_8)));
return bytesToHex(salt) + ":" + hash;
}
/**
* 驗證輸入是否匹配存儲的鹽值雜湊
*/
public static boolean verify(String input, String stored) {
String[] parts = stored.split(":");
if (parts.length != 2) return false;
byte[] salt = hexToBytes(parts[0]);
String hash = parts[1];
String computed = md5(concat(salt, input.getBytes(StandardCharsets.UTF_8)));
return MessageDigest.isEqual(hash.getBytes(), computed.getBytes()); // 恆定時間比較
}
private static byte[] concat(byte[] a, byte[] b) {
byte[] result = new byte[a.length + b.length];
System.arraycopy(a, 0, result, 0, a.length);
System.arraycopy(b, 0, result, a.length, b.length);
return result;
}
// 復用上文 bytesToHex、hexToBytes、md5 方法...
}
效能優化:大文件流式計算
public static String md5File(Path filePath) throws IOException {
try (InputStream is = Files.newInputStream(filePath);
DigestInputStream dis = new DigestInputStream(is, MessageDigest.getInstance("MD5"))) {
byte[] buffer = new byte[8192];
while (dis.read(buffer) != -1) {
// 僅為推進流,摘要自動累計
}
return bytesToHex(dis.getMessageDigest().digest());
} catch (NoSuchAlgorithmException e) {
throw new IllegalStateException(e);
}
}
常見坑點與最佳實踐
| 坑點 | 後果 | 正確做法 |
| 直接 `new String(bytes)` 轉字符串 | 平台編碼導致結果不一致 | 顯式指定 UTF-8 或用十六進制編碼 |
| 用於密碼存儲 | 極易被彩虹表/GPU 破解 | **必須用 BCrypt/SCrypt/Argon2 + 鹽值** |
| 忽略大小寫比較 | 潛在邏輯錯誤 | 統一輸出小寫,比較時用 equalsIgnoreCase |
| 頻繁創建 MessageDigest | 線程不安全、效能差 | ThreadLocal 復用或每次獲取新實例 |
現代替代方案對比