MD5 (Message Digest Algorithm 5) 是廣泛使用的雜湊函數,雖已不適合密碼學安全場景,但在檔案完整性校驗、非安全標識生成、快取鍵值等領域仍有廣泛應用。本文提供三種 Java 實作方式、效能基準測試、安全性分析與遷移建議。
| 屬性 | 值 |
|---|---|
| 輸出長度 | 128 bits (16 bytes / 32 hex chars) |
| 演算法類型 | 單向雜湊函數 |
| 碰撞抗性 | 已破解 (2004 年王小雲團隊) |
| 預像抗性 | 理論安全,但計算成本極低 |
| Java 提供者 | SunJCE (預設)、BouncyCastle |
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
public class Md5Util {
private static final char[] HEX_DIGITS = "0123456789abcdef".toCharArray();
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 algorithm not available", e);
}
}
// 高效能十六進位轉換 (避免 StringBuilder 擴容)
private static String bytesToHex(byte[] bytes) {
char[] result = new char[bytes.length * 2];
for (int i = 0, j = 0; i < bytes.length; i++) {
byte b = bytes[i];
result[j++] = HEX_DIGITS[(b >> 4) & 0xF];
result[j++] = HEX_DIGITS[b & 0xF];
}
return new String(result);
}
// 檔案 MD5 (流式處理,避免載入整個檔案)
public static String md5File(Path file) throws IOException {
try (InputStream is = Files.newInputStream(file)) {
MessageDigest md = MessageDigest.getInstance("MD5");
byte[] buffer = new byte[8192];
int read;
while ((read = is.read(buffer)) != -1) {
md.update(buffer, 0, read);
}
return bytesToHex(md.digest());
} catch (NoSuchAlgorithmException e) {
throw new IllegalStateException(e);
}
}
}
commons-codec
commons-codec
1.16.0
// 使用
import org.apache.commons.codec.digest.DigestUtils;
public class Md5Commons {
public static String md5(String input) {
return DigestUtils.md5Hex(input.getBytes(StandardCharsets.UTF_8));
}
public static String md5File(File file) throws IOException {
try (FileInputStream fis = new FileInputStream(file)) {
return DigestUtils.md5Hex(fis);
}
}
}
com.google.guava
guava
32.1.3-jre
// 使用
import com.google.common.hash.Hashing;
public class Md5Guava {
public static String md5(String input) {
return Hashing.md5()
.hashString(input, StandardCharsets.UTF_8)
.toString();
}
public static String md5Bytes(byte[] input) {
return Hashing.md5().hashBytes(input).toString();
}
}
@BenchmarkMode(Mode.Throughput)
@OutputTimeUnit(TimeUnit.SECONDS)
@State(Scope.Thread)
@Warmup(iterations = 3, time = 1)
@Measurement(iterations = 5, time = 1)
@Fork(1)
public class Md5Benchmark {
@Param({"100", "1000", "10000"})
int inputSize;
byte[] input;
@Setup
public void setup() {
input = new byte[inputSize];
new Random().nextBytes(input);
}
@Benchmark
public String jdkMd5() {
return Md5Util.md5(input);
}
@Benchmark
public String commonsMd5() {
return Md5Commons.md5(new String(input, StandardCharsets.UTF_8));
}
@Benchmark
public String guavaMd5() {
return Md5Guava.md5Bytes(input);
}
}
| 輸入大小 | JDK 原生 | Commons Codec | Guava | 勝出者 |
|---|---|---|---|---|
| 100 bytes | ~2,800,000 | ~2,600,000 | ~2,500,000 | JDK 原生 |
| 1 KB | ~450,000 | ~420,000 | ~400,000 | JDK 原生 |
| 10 KB | ~55,000 | ~52,000 | ~50,000 | JDK 原生 |
結論:JDK 原生效能最佳,無額外依賴。Commons Codec/Guava 差異 < 10%,主要優勢在 API 易用性與跨平台一致性。
| 場景 | 風險 | 建議替代 |
|---|---|---|
| 密碼儲存 | 彩虹表、GPU 破解、碰撞攻擊 | BCrypt, Argon2, PBKDF2, Scrypt |
| Token/Session ID 生成 | 可預測、碰撞 | SecureRandom + Base64, UUID |
| 數位簽名/完整性驗證 | 碰撞攻擊可偽造 | SHA-256, SHA-3, BLAKE2 |
| SSL/TLS 憑證 | 已禁用 | SHA-256+ |
org.mindrot
jbcrypt
0.4
// 使用
import org.mindrot.jbcrypt.BCrypt;
public class PasswordUtil {
// 成本因子 12 (約 250ms/次,可根據硬體調整 10-14)
private static final int COST = 12;
public static String hash(String plainPassword) {
return BCrypt.hashpw(plainPassword, BCrypt.gensalt(COST));
}
public static boolean verify(String plainPassword, String hashed) {
return BCrypt.checkpw(plainPassword, hashed);
}
}
public class SaltedMd5 {
private static final SecureRandom RANDOM = new SecureRandom();
public static String generateSalt() {
byte[] salt = new byte[16];
RANDOM.nextBytes(salt);
return Md5Util.bytesToHex(salt);
}
public static String md5WithSalt(String input, String saltHex) {
byte[] salt = Md5Util.hexToBytes(saltHex);
byte[] inputBytes = input.getBytes(StandardCharsets.UTF_8);
byte[] combined = new byte[salt.length + inputBytes.length];
System.arraycopy(salt, 0, combined, 0, salt.length);
System.arraycopy(inputBytes, 0, combined, salt.length, inputBytes.length);
return Md5Util.md5(combined);
}
public static boolean verify(String input, String saltHex, String expectedMd5) {
return md5WithSalt(input, saltHex).equals(expectedMd5);
}
}
public class LargeFileMd5 {
public static String calculate(Path file) throws IOException {
try (InputStream is = Files.newInputStream(file)) {
MessageDigest md = MessageDigest.getInstance("MD5");
byte[] buffer = new byte[1024 * 1024]; // 1MB buffer
int read;
while ((read = is.read(buffer)) != -1) {
md.update(buffer, 0, read);
}
return Md5Util.bytesToHex(md.digest());
} catch (NoSuchAlgorithmException e) {
throw new IllegalStateException(e);
}
}
// 並行計算多個檔案
public static Map calculateParallel(List files) throws InterruptedException, ExecutionException {
try (ExecutorService executor = Executors.newFixedThreadPool(
Math.min(files.size(), Runtime.getRuntime().availableProcessors()))) {
List>> futures = files.stream()
.map(file -> CompletableFuture.supplyAsync(() -> {
try {
return Map.entry(file, calculate(file));
} catch (IOException e) {
throw new CompletionException(e);
}
}, executor))
.collect(Collectors.toList());
return futures.stream()
.map(CompletableFuture::join)
.collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue));
}
}
}
public class ThreadLocalMd5 {
private static final ThreadLocal MD5_DIGEST = ThreadLocal.withInitial(() -> {
try {
return MessageDigest.getInstance("MD5");
} catch (NoSuchAlgorithmException e) {
throw new IllegalStateException(e);
}
});
public static String md5(byte[] input) {
MessageDigest md = MD5_DIGEST.get();
md.reset();
return Md5Util.bytesToHex(md.digest(input));
}
}
Java 實現 MD5 首選 JDK 原生 MessageDigest,無依賴、效能最佳。嚴禁用於密碼儲存、Token 生成、安全簽名。密碼請用 BCrypt/Argon2,完整性驗證請用 SHA-256,Token 請用 SecureRandom。理解 MD5 的定位邊界,是開發者基本的安全素養。