MD5 (Message Digest Algorithm 5) 曾廣泛用於密碼存儲、文件校驗、數字簽名。但自 2004 年發現碰撞攻擊以來,MD5 已不適合安全敏感場景。本文提供標準實現、安全性分析、鹽值加固方案及現代替代建議。
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
public final class Md5Utils {
private Md5Utils() {}
/**
* 計算字符串 MD5 (十六進制小寫, 32 位)
*/
public static String md5(String input) {
return md5(input, "UTF-8");
}
/**
* 計算字符串 MD5 (指定編碼)
*/
public static String md5(String input, String charset) {
if (input == null) return null;
try {
MessageDigest md = MessageDigest.getInstance("MD5");
byte[] digest = md.digest(input.getBytes(charset));
return bytesToHex(digest);
} catch (Exception e) {
throw new RuntimeException("MD5 計算失敗", e);
}
}
/**
* 計算文件 MD5 (流式處理, 防 OOM)
*/
public static String md5File(File file) {
try (FileInputStream fis = new FileInputStream(file)) {
MessageDigest md = MessageDigest.getInstance("MD5");
byte[] buffer = new byte[8192];
int len;
while ((len = fis.read(buffer)) != -1) {
md.update(buffer, 0, len);
}
return bytesToHex(md.digest());
} catch (Exception e) {
throw new RuntimeException("文件 MD5 計算失敗: " + file, e);
}
}
private static String bytesToHex(byte[] bytes) {
StringBuilder sb = new StringBuilder(bytes.length * 2);
for (byte b : bytes) {
sb.append(String.format("%02x", b));
}
return sb.toString();
}
// 測試
public static void main(String[] args) {
System.out.println("MD5('LXD') = " + md5("LXD")); // 32位
System.out.println("16位截取 = " + md5("LXD").substring(8, 24)); // 中間16位
}
}
| 攻擊類型 | 年份 | 複雜度 | 影響 |
|---|---|---|---|
| 碰撞攻擊 | 2004 | 2^39 | 可構造兩個不同輸入產生相同 MD5 |
| 選擇前綴碰撞 | 2007 | 2^50 | 可偽造數字證書 (如 Flame 病毒) |
| 單次碰撞 | 2013 | 2^18 | 幾秒內生成碰撞對 |
| 預像攻擊 | 理論 | 2^123 | 仍較困難,但邊緣在收窄 |
駭客預計算海量常見密碼的 MD5,建立「彩虹表」反查:
123456, password 等) 秒級破解// 為每個用戶生成唯一隨機鹽
public static String generateSalt() {
byte[] salt = new byte[16];
new SecureRandom().nextBytes(salt);
return Base64.getEncoder().encodeToString(salt);
}
// 驗證:md5(salt + password) 或 md5(password + salt)
public static String md5WithSalt(String password, String salt) {
return md5(salt + password); // 或 password + salt
}
public static String md5Iterated(String input, int iterations) {
String hash = input;
for (int i = 0; i < iterations; i++) {
hash = md5(hash);
}
return hash;
}
// 建議 iterations >= 10000 (約幾十毫秒/次)
// 存儲格式: $MD5$iterations$salt$hash
// 新用戶用 bcrypt/Argon2, 舊用戶登錄時驗證並升級
public boolean verifyAndUpgrade(String inputPassword, String storedHash) {
if (storedHash.startsWith("$MD5$")) {
// 解析參數驗證
if (verifyMd5(inputPassword, storedHash)) {
// 升級為 bcrypt
String newHash = BCrypt.hashpw(inputPassword, BCrypt.gensalt(12));
updateUserPasswordHash(newHash);
return true;
}
return false;
} else if (storedHash.startsWith("$2a$")) {
return BCrypt.checkpw(inputPassword, storedHash);
}
return false;
}
| 算法 | 類型 | 參數 | Java 支援 | 推薦場景 |
|---|---|---|---|---|
| Argon2id | 記憶體困難 | 時間、記憶體、並行度 | Bouncy Castle, Argon2-JVM | 最高安全要求、新系統首選 |
| bcrypt | 計算困難 | 成本因子 (4-31) | Spring Security, jBCrypt | 廣泛支援、成熟穩定、Web 應用標準 |
| PBKDF2 | 計算困難 | 迭代次數、雜湊算法 | JDK 內建 PBKDF2WithHmacSHA256 | 標準合規、FIPS 認證環境 |
| scrypt | 記憶體困難 | N, r, p | Bouncy Castle | 記憶體受限設備、輕量級 |
| MD5/SHA | 快速雜湊 | 無 | JDK 內建 | 僅用於非安全場景: 文件校驗、緩存鍵、去重 |
@Configuration
public class SecurityConfig {
@Bean
public PasswordEncoder passwordEncoder() {
// 委派編碼器:自動識別前綴,支援多算法共存平滑遷移
Map encoders = new HashMap<>();
encoders.put("bcrypt", new BCryptPasswordEncoder(12)); // 推薦成本 12
encoders.put("argon2", new Argon2PasswordEncoder(16, 32, 1, 65536, 4));
encoders.put("pbkdf2", new Pbkdf2PasswordEncoder("", 100000, 256));
encoders.put("MD5", new MessageDigestPasswordEncoder("MD5")); // 兼容舊數據
return new DelegatingPasswordEncoder("bcrypt", encoders);
}
}
MD5 已退出安全舞台。新系統請直接使用 bcrypt (成本 12+) 或 Argon2id。存量系統請實施鹽值+迭代+委派編碼器漸進遷移。記住:雜湊慢才是安全,快速雜湊是給校驗用的,不是給密碼用的。