MD5(Message Digest Algorithm 5)雖已不適合密碼學安全場景(碰撞攻擊、彩虹表),但在文件完整性校驗、非敏感數據指紋生成、緩存 Key 構建、幂等性 Token 等工程領域仍被廣泛使用。Java 標準庫 java.security.MessageDigest 提供了原生實現,無需第三方依賴。本文梳理常見陷阱,提供生產級工具類。
MessageDigest md5 = MessageDigest.getInstance("MD5");
byte[] digest = md5.digest(inputBytes); // 一次性計算
// 或分塊更新
md5.update(chunk1);
md5.update(chunk2);
byte[] digest = md5.digest();
錯誤寫法:Integer.toHexString(bytes[i]) 直接轉換,負數會產生 8 位十六進制(如 ffffffab),導致長度不固定、前導 0 丟失。
正確寫法:byte & 0xFF 取無符號值,再補齊兩位。
private static String bytesToHex(byte[] bytes) {
StringBuilder sb = new StringBuilder(bytes.length * 2);
for (byte b : bytes) {
int v = b & 0xFF; // 關鍵:無符號化
if (v < 0x10) sb.append('0'); // 補齊高位 0
sb.append(Integer.toHexString(v));
}
return sb.toString(); // 32 位小寫
}
str.getBytes() vs str.getBytes(StandardCharsets.UTF_8)默認編碼隨平台變化(Windows GBK,Linux UTF-8),導致同樣字符串在不同環境產生不同 MD5。**務必顯式指定編碼**。
public final class MD5Util {
private MD5Util() {} // 工具類禁止實例化
/** 32 位小寫 MD5 */
public static String md5Hex(String input) {
return md5Hex(input, StandardCharsets.UTF_8);
}
/** 32 位小寫 MD5(指定編碼) */
public static String md5Hex(String input, Charset charset) {
if (input == null) return null;
try {
MessageDigest md = MessageDigest.getInstance("MD5");
byte[] digest = md.digest(input.getBytes(charset));
return bytesToHex(digest);
} catch (NoSuchAlgorithmException e) {
throw new IllegalStateException("JDK 缺少 MD5 算法", e); // 不可能發生
}
}
/** 32 位大寫 MD5 */
public static String md5HexUpper(String input) {
String h = md5Hex(input);
return h == null ? null : h.toUpperCase();
}
/** 16 位 MD5(取中間 16 字符,小寫) */
public static String md5Hex16(String input) {
String h = md5Hex(input);
return h == null ? null : h.substring(8, 24);
}
/** 文件 MD5 校驗(流式處理,避免 OOM) */
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);
}
}
// ===== 內部實現 =====
private static String bytesToHex(byte[] bytes) {
char[] hexChars = "0123456789abcdef".toCharArray();
char[] result = new char[bytes.length * 2];
for (int i = 0, j = 0; i < bytes.length; i++) {
int v = bytes[i] & 0xFF;
result[j++] = hexChars[v >>> 4];
result[j++] = hexChars[v & 0x0F];
}
return new String(result);
}
}
// 字符串加密
String raw = "user:123:action:login";
String token = MD5Util.md5Hex(raw); // 32 位小寫
String tokenUpper = MD5Util.md5HexUpper(raw); // 32 位大寫
String shortToken = MD5Util.md5Hex16(raw); // 16 位
// 文件校驗
Path file = Paths.get("/data/app.apk");
String fileMd5 = MD5Util.md5File(file);
System.out.println("APK MD5: " + fileMd5);
// 緩存 Key 生成
String cacheKey = "cache:" + MD5Util.md5Hex(userId + ":" + requestParams);
| 場景 | 是否推薦 MD5 | 推薦替代 |
|---|---|---|
| 用戶密碼存儲 | ❌ 絕對禁止 | BCrypt / Argon2 / PBKDF2(帶鹽、慢哈希) |
| 敏感 Token 簽名 | ❌ 不安全 | HMAC-SHA256 / RSA/ECDSA 簽名 |
| 文件完整性校驗(非惡意篡改) | ✅ 可接受 | SHA-256 更安全 |
| 緩存 Key、去重指紋、幂等 Key | ✅ 極佳選擇 | — |
| 數據庫分庫分表路由 Key | ✅ 常用 | — |
MessageDigest 非線程安全,但可使用 ThreadLocal 復用實例,減少對象創建開銷。new BigInteger(1, digest).toString(16) 需手動補 0,且性能差於位運算拼接。DigestInputStream 邊讀邊算,內存佔用恆定(僅緩衝區大小)。Java 原生 MessageDigest 實現 MD5 簡單高效,關鍵在於:顯式指定 UTF-8 編碼、正確處理字節符號位轉十六進制、根據場景選擇 32/16 位大小寫。工具類封裝後可直接用於緩存 Key、文件校驗、路由分片等高頻場景。**切記:密碼存儲、簽名驗證等安全敏感場景嚴禁使用 MD5**,請改用 BCrypt、HMAC-SHA256 等現代密碼學方案。