Skip to content

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 md5
javascript
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 bcrypt
javascript
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 / false

bcrypt 自动生成随机盐 + 慢哈希,自带防彩虹表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 / ETagMD5 仍可用不需要安全,只需要去重

六、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 / ETagmd5(非安全场景仍可用)
老系统升级登录时检测 MD5 自动升级 bcrypt

九、小结

  • MD5 是单向 hash,不是加密;固定 32 字符输出
  • 不再适合安全场景:密码、签名、证书——已被攻破
  • 仍适合非安全场景:文件指纹、缓存 key、ETag
  • 密码存储用 bcrypt / Argon2;API 签名用 HMAC-SHA256
  • 老项目的 MD5 密码可在登录时静默升级到 bcrypt,无需重置