密码加密还在用 MD5?2026 年了,换成 BCrypt 吧
摘要:MD5 曾经是好选择,但早在十年前就已经不再安全。一文说清为什么你的项目必须从 MD5 切换到 BCrypt,以及怎么改。
如果你写后端代码时,密码加密的第一反应还是 md5(password),这篇文章就是写给你看的。
不是因为你技术不行,而是 MD5 实在太"有名"了——有名到几乎所有初学者都会第一个想到它,而有史以来最大的安全问题,往往就藏在这种"大家都这么用"的习惯里。
一、MD5 到底怎么了?
MD5 的设计初衷是校验文件完整性,不是用来加密密码的。把它拿来存密码,就像拿卷尺称体重——工具本身没问题,但你用错了场景。
MD5 有三个致命问题:
1. 太快了——快得离谱
MD5 计算一次只需要几微秒。一块普通的 GPU 每秒可以跑上百亿次 MD5 运算。
这意味着什么?如果你的用户密码是 123456,黑客拿到数据库后,用字典+暴力破解,几分钟就能跑完。密码复杂度稍高一点?也就是几个小时的区别。
好的密码哈希算法必须慢。慢到攻击者算一个密码要花几百毫秒,但你的用户登录时等个 200 毫秒无感知。这是安全设计里最反直觉也最重要的一点。
2. 碰撞攻击已被彻底破解
2004 年,王小云教授就公开了 MD5 碰撞攻击方法。虽然对密码场景的直接影响有限,但这已经给 MD5 判了死刑——一个能被构造碰撞的哈希函数,不值得任何信任。
3. 彩虹表可以预先计算所有可能
因为 MD5 没有盐值(salt),黑客可以直接下载现成的彩虹表,把常见密码的 MD5 值全部映射好,拿到数据库后秒查秒出。
即使你加了 salt,只要用 md5(password + salt),仍然挡不住现代 GPU 的暴力破解速度。
二、BCrypt 为什么更好?
BCrypt 是 1999 年 Niels Provos 和 David Mazières 基于 Blowfish 密码设计的专门用于密码哈希的算法。它从根子上解决了 MD5 的问题。
核心优势一:可调的工作因子
BCrypt 内置一个 cost(工作因子)参数,决定了它要做多少轮迭代。
cost = 10 → 约 100ms
cost = 12 → 约 400ms
cost = 14 → 约 1.6s
随着硬件越来越强,你只需要调大这个数字,安全性就会跟着涨。这是 MD5 永远做不到的——MD5 的速度是固定的,你没办法让它变慢。
核心优势二:内置盐值,无需手动管理
每次调用 BCrypt,它会自动生成一个随机盐值,并把盐值、cost、算法标识一起编码进结果字符串:
$2b$12$WApznULhK2q3mxWgNpBwHe7BkPQl8sKQ0YmKzGJhN2Fq7GnJvX3yO
^ ^ ^________________ 盐值 + 哈希结果
| |
| └─ cost = 12
└──── 算法版本
你不需要单独存盐值字段,验证时 BCrypt 自己从字符串里提取。
核心优势三:经过 25 年实战检验
BCrypt 从 1999 年用到现在,是 OpenBSD、Linux 发行版、Rails、Spring Security 等无数项目的默认密码算法。没有被实质性攻破过。
三、怎么从 MD5 切换到 BCrypt?
第一步:引入 BCrypt 库
根据你的技术栈选择对应实现:
| 语言/框架 | 推荐库 |
|---|---|
| Java / Spring | Spring Security BCrypt(内置) |
| Node.js | bcrypt 或 bcryptjs |
| Python | bcrypt(PyPI) |
| Go | golang.org/x/crypto/bcrypt |
| PHP | password_hash()(PHP 5.5+ 内置) |
第二步:新用户直接用 BCrypt
// Java / Spring Security
String hashed = BCrypt.hashpw(plainPassword, BCrypt.gensalt(12));
// Node.js
const hashed = await bcrypt.hash(plainPassword, 12);
# Python
hashed = bcrypt.hashpw(plainPassword.encode(), bcrypt.gensalt(12))
第三步:老用户平滑迁移——双哈希策略
这是最关键的环节。你不可能强制所有用户重置密码,所以需要双哈希兼容:
验证逻辑改成这样:
- 先尝试用 BCrypt 验证密码
- 如果 BCrypt 验证失败,用 MD5 验证(兼容老数据)
- 如果 MD5 验证通过,立刻用 BCrypt 重新哈希并更新数据库
public boolean verifyAndMigrate(String userId, String plainPassword) {
User user = userDao.findById(userId);
// 1. BCrypt 优先
if (BCrypt.checkpw(plainPassword, user.getPasswordHash())) {
return true;
}
// 2. 降级到 MD5 验证(老用户)
String md5Hash = md5(plainPassword);
if (md5Hash.equals(user.getPasswordHash())) {
// 3. 验证通过,立即升级为 BCrypt
String newHash = BCrypt.hashpw(plainPassword, BCrypt.gensalt(12));
userDao.updatePasswordHash(userId, newHash);
return true;
}
return false;
}
这样每次老用户登录时,密码就会被自动迁移到 BCrypt。几周到几个月后,绝大多数活跃用户都完成了升级。
第四步:清理残留的 MD5 数据
跑一个查询,找出还没迁移的用户,强制他们重置密码:
SELECT id, email FROM users
WHERE password_hash NOT LIKE '$2b$%';
四、几个常见误区
"我加了 salt 的 MD5 就安全了"
加了 salt 确实防住了彩虹表,但挡不住暴力破解。MD5 太快了,GPU 每秒百亿次运算,盐值只能增加一点点成本。
"我的用户量少,没人会来破解"
数据泄露从来不是因为"有人盯上了你",而是数据库被批量拖走后,黑客顺手就跑了一遍。小网站的数据库一样在黑市上流通。
"BCrypt 太慢了,影响用户体验"
cost=12 大约 200-400ms,用户登录时根本感知不到。但攻击者需要多花几十万倍的时间。让好人等 0.2 秒,让坏人等 100 年,这就是密码哈希的设计哲学。
五、总结
| MD5 | BCrypt | |
|---|---|---|
| 设计用途 | 文件校验 | 密码哈希 |
| 速度 | 极快(危险) | 可调,默认慢 |
| 盐值 | 手动管理 | 内置自动 |
| 抗暴力破解 | 不行 | 强 |
| 抗彩虹表 | 不加 salt 不行 | 天生免疫 |
| 安全性评级 | ❌ 不推荐 | ✅ 行业标准 |
2026 年了,如果你的项目还在用 MD5 存密码,今天就是该换的日子。
不需要重写整个系统,不需要强制用户改密码,按照上面的双哈希策略,渐进式迁移,一周就能搞定。
安全从来不是一步到位的,但每一步都应该走在正确的方向上。