密码加密还在用 MD5?2026 年了,换成 BCrypt 吧

14 阅读4分钟

密码加密还在用 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 / SpringSpring Security BCrypt(内置)
Node.jsbcryptbcryptjs
Pythonbcrypt(PyPI)
Gogolang.org/x/crypto/bcrypt
PHPpassword_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))

第三步:老用户平滑迁移——双哈希策略

这是最关键的环节。你不可能强制所有用户重置密码,所以需要双哈希兼容

验证逻辑改成这样:

  1. 先尝试用 BCrypt 验证密码
  2. 如果 BCrypt 验证失败,用 MD5 验证(兼容老数据)
  3. 如果 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 年,这就是密码哈希的设计哲学。


五、总结

MD5BCrypt
设计用途文件校验密码哈希
速度极快(危险)可调,默认慢
盐值手动管理内置自动
抗暴力破解不行
抗彩虹表不加 salt 不行天生免疫
安全性评级❌ 不推荐✅ 行业标准

2026 年了,如果你的项目还在用 MD5 存密码,今天就是该换的日子。

不需要重写整个系统,不需要强制用户改密码,按照上面的双哈希策略,渐进式迁移,一周就能搞定。


安全从来不是一步到位的,但每一步都应该走在正确的方向上。