Node系列 · ORM:MD5 加密
MD5 是 Node 后端最早接触的"加密"工具——给密码做哈希、给文件生成指纹。但 MD5 在 2004 年已被攻破,不再适合用于安全场景。本章讲清楚 MD5 的本质、它在哪些场景能用、哪些场景必须换方案。
一、MD5 是什么
MD5(Message-Digest Algorithm 5)是一种单向 hash 函数:
| 特性 | 说明 |
|---|---|
| 输入 | 任意长度字符串 |
| 输出 | 固定 128 位(32 个十六进制字符) |
| 单向 | 无法从结果反推输入 |
| 确定性 | 同一输入永远产生同一输出 |
| 快速 | 大量数据也能毫秒级算出 |
bash
$ echo -n 'hello' | md5sum
5d41402abc4b2a76b9719d911017c592无论输入是 5 字节还是 5 GB,输出永远是 32 个字符。
二、Node 中使用 MD5
bash
npm install md5javascript
const md5 = require('md5');
md5('hello');
// '5d41402abc4b2a76b9719d911017c592'
md5('hello').length;
// 32(固定长度)2.1 密码哈希
javascript
// 存储密码
const hashedPassword = md5(password);
// 验证
if (md5(inputPassword) === storedHash) {
// 密码正确
}⚠️ 这样用不安全——见下节。
三、MD5 的真实用途
3.1 ✅ 适合用 MD5
| 场景 | 原因 |
|---|---|
| 文件指纹 / 校验和 | 验证文件完整性(下载校验、镜像比对) |
| 非安全去重 | 给文件 / 内容生成唯一 ID(如头像去重) |
| ETag 生成 | HTTP 缓存标识,不需要防碰撞 |
| 非敏感数据的快速哈希 | 如日志指纹、临时缓存 key |
javascript
const md5 = require('md5');
const fs = require('fs');
// 计算文件指纹
const hash = md5(fs.readFileSync('./big-file.zip'));
console.log(`SHA1: ${hash}`); // 可用于校验下载完整性3.2 ❌ 不要用 MD5 的场景
| 场景 | 风险 | 替代方案 |
|---|---|---|
| 密码存储 | 彩虹表 / GPU 爆破 | bcrypt / Argon2 |
| 数字签名 | 碰撞攻击(2004 年已实现) | SHA-256 / SHA-3 |
| SSL / TLS 证书 | CA 早就不再签 MD5 证书 | SHA-256 |
| API 签名参数 | 可被碰撞构造 | HMAC-SHA256 |
| 加密敏感数据 | MD5 不是加密算法,是哈希 | AES / ChaCha20 |
四、MD5 的安全缺陷
4.1 彩虹表攻击
MD5 输出固定 32 字符——可以预先计算所有常见密码的 MD5:
javascript
// 攻击者的彩虹表
const rainbowTable = {
'5d41402abc4b2a76b9719d911017c592': 'hello',
'e10adc3949ba59abbe56e057f20f883e': '123456',
'd8578edf8458ce06fbc5bb76a58c5ca4': 'qwerty',
// ... 几十亿条
};
// 用户登录时
if (rainbowTable[md5(input)]) {
console.log('密码是:', rainbowTable[md5(input)]);
}存储 MD5 哈希等于把密码明文交给攻击者——只要他有一份彩虹表。
4.2 碰撞攻击
2004 年,王小云教授团队发布了对 MD5 的实际碰撞攻击:
javascript
// 两个不同的输入产生相同的 MD5
const a = 'd131dd02c5e6eec4693d9a0698aff95c2fcab58712467eab4004583eb8f7f89a4';
const b = 'd131dd02c5e6eec4693d9a0698aff95c2fcab50712467eab4004583eb8f7f89a4';
md5(a) === md5(b); // true,但 a !== b这意味着不能用 MD5 做"文件唯一性"或"签名验证"——攻击者能构造出"看起来不同但哈希相同"的内容。
4.3 GPU 爆破
现代 GPU 一秒能算 数十亿次 MD5:
bash
# 8 字符密码(纯字母数字),GPU 一小时内能穷举完
hashcat -m 0 -a 3 hash.txt '?l?l?l?l?l?l?l?l'五、正确替代方案
5.1 密码存储:bcrypt / Argon2
bash
npm install bcryptjavascript
const bcrypt = require('bcrypt');
// 注册:加盐 + 慢哈希(耗时约 100ms,攻击者无法高速爆破)
const saltRounds = 10;
const hash = await bcrypt.hash(password, saltRounds);
// $2b$10$N9qo8uLOickgx2ZMRZoMyeIjZAgcfl7p92ldGxad68LJZdL17lhWy
// 登录:验证
const match = await bcrypt.compare(inputPassword, hash);
// true / falsebcrypt 自动生成随机盐 + 慢哈希,自带防彩虹表。Argon2 是更新的方案,更安全但 Node 生态还在普及。
5.2 文件指纹 / API 签名:SHA-256
javascript
const crypto = require('crypto');
const hash = crypto.createHash('sha256').update(data).digest('hex');
// HMAC-SHA256:带密钥的 hash,API 签名常用
const hmac = crypto.createHmac('sha256', secretKey).update(payload).digest('hex');crypto 是 Node 内置,无需安装。
5.3 选型对照
| 场景 | 推荐 | 原因 |
|---|---|---|
| 密码存储 | bcrypt / Argon2 | 加盐 + 慢哈希,防彩虹表 / GPU |
| 密码校验(数据库存了 MD5 老数据) | 升级方案:登录时检测 MD5,重置后用 bcrypt | 兼容期可临时保留 |
| 文件指纹 | SHA-256 | 无碰撞风险,速度比 SHA-1 / MD5 慢但安全 |
| API 签名 | HMAC-SHA256 | 带密钥的 hash,无法伪造 |
| 缓存 key / ETag | MD5 仍可用 | 不需要安全,只需要去重 |
六、MD5 升级路径
老项目里如果用了 MD5 存密码,迁移步骤:
javascript
const bcrypt = require('bcrypt');
const md5 = require('md5');
// 1. 用户登录时检测
async function login(email, inputPassword) {
const user = await User.findOne({ where: { email } });
if (!user) return null;
// 2. 检查哈希格式
const isMD5 = /^[a-f0-9]{32}$/.test(user.password);
let isValid;
if (isMD5) {
// 老数据:用 MD5 验证
isValid = md5(inputPassword) === user.password;
// 3. 验证通过后升级到 bcrypt
if (isValid) {
const newHash = await bcrypt.hash(inputPassword, 10);
await user.update({ password: newHash });
}
} else {
// 新数据:bcrypt 验证
isValid = await bcrypt.compare(inputPassword, user.password);
}
return isValid ? user : null;
}这样所有用户登录时都会被静默升级到 bcrypt——无需重置密码。
七、常见误解
| 误解 | 真相 |
|---|---|
| "MD5 是加密算法" | MD5 是哈希,不是加密;不可逆 |
| "MD5 加盐就安全了" | 自定义"盐"方案不安全;用 bcrypt / Argon2 |
| "MD5 长度固定,反推不出来" | 彩虹表 + GPU 爆破可以秒破短密码 |
| "双重 MD5 更安全" | 不增加安全性,只是计算量翻倍 |
| "截断 MD5(如 md5(md5(x))[0:16])省空间" | 进一步削弱安全性 |
八、最佳实践
| 场景 | 推荐 |
|---|---|
| 密码存储 | bcrypt.hash / bcrypt.compare |
| 文件指纹 | crypto.createHash('sha256') |
| API 签名 | crypto.createHmac('sha256', key) |
| 缓存 key / ETag | md5(非安全场景仍可用) |
| 老系统升级 | 登录时检测 MD5 自动升级 bcrypt |
九、小结
- MD5 是单向 hash,不是加密;固定 32 字符输出
- 不再适合安全场景:密码、签名、证书——已被攻破
- 仍适合非安全场景:文件指纹、缓存 key、ETag
- 密码存储用
bcrypt/Argon2;API 签名用HMAC-SHA256 - 老项目的 MD5 密码可在登录时静默升级到 bcrypt,无需重置
