密码该用 bcrypt 还是 SHA-256

编码与加密发布于 2026-10-31

同一个哈希函数,用在两个地方结论相反:

  • 校验文件完整性 → 要快,SHA-256 正确
  • 存储用户密码 → 要慢,SHA-256 是错的

区别在于对手是谁、攻击方式是什么。

为什么快哈希不能存密码

假设数据库泄露,里面存的是 sha256(password)。攻击者要做的是「猜出原文」:

哈希方式 单张高端显卡每秒能试 8 位混合字符口令(约 2×10^15 种)耗时
SHA-256 约 10^10 次 数天到数周
bcrypt(cost 12) 约 10^4 次 数千年

SHA 的设计目标就是快(大量数据校验、矿池、签名),GPU 上并行度极高。bcrypt 的设计目标恰好相反:故意让每次计算耗时可调,让暴力枚举的边际成本高到不可行。

所以问题不是「SHA 不安全」,而是「它太快了,快得帮了攻击者」。

盐解决的不是「慢」

加盐防的是另一件事:彩虹表和预计算。

不加盐时,123456 的 SHA-256 永远是同一个值,攻击者可以预先算好千万条常见口令的哈希,直接查表。同一个站的两个用户密码相同时,哈希也相同,一破全破。

加盐后,每个密码配一个随机值参与运算,同样的口令得到不同哈希,预计算表全部失效。

hash = SHA256(salt + password)

但注意:加盐不能让快哈希变安全。攻击者知道了盐(盐是明文存在库里的),仍然可以逐条暴力试,只是每条都要重算一次。盐提升的是「批量破解」的成本,不是「单条破解」的成本。要提升单条成本,必须让哈希本身变慢。

bcrypt 的三个实操细节

cost 该设多少

cost 是 2 的指数:cost 12 意味着 2^12 = 4096 轮。每加 1,耗时翻倍。

cost 单次耗时(大致) 建议
10 50~100ms 最低可接受
12 200~400ms 推荐默认
14 1~2s 高安全场景,登录会明显变慢

原则:让单次校验落在 200~500ms。低于这个数,攻击者的优势变大;高于 1s,用户体验开始疼,还可能招来针对性的资源耗尽攻击。

代价是所有登录请求都会占用 CPU 200ms 以上。并发高时要限流,否则登录接口本身成了 DoS 入口。

72 字节上限

bcrypt 只取密码的前 72 字节,超出部分静默丢弃。

这意味着两个很长的密码可能等价。中文用户尤其容易踩到:UTF-8 下一个汉字 3 字节,24 个汉字就到顶了。

处理办法:先对密码做一次 SHA-256 再 base64(得到固定 44 字符),再交给 bcrypt。这被称为「预哈希」,要注意两步都用 UTF-8 且不截断。

校验必须用库提供的 compare

const hash = await bcrypt.hash(password, 12);
// 存在库里的就是 hash,形如 $2b$12$...

// 校验
await bcrypt.compare(inputPassword, hash);   // 对

不要自己拆出 salt 再拼一遍 —— bcrypt 的 salt 和 cost 都编码在结果字符串里,手拼容易漏掉版本号($2a$ 和 $2b$ 行为有细微差别)。

同理,比对哈希不能用 ===(这条适用于所有场景):字符串比较会在第一个不同的字符处提前返回,耗时差异可以被用来逐字符猜哈希。时间安全比较(crypto.timingSafeEqual)是正确做法。

现在更推荐 Argon2

bcrypt 的问题是不够「内存硬」:它消耗 CPU 但内存占用很小,而 GPU、FPGA 恰好擅长这种计算。

Argon2(2015 年密码哈希竞赛冠军)要求同时消耗内存和时间,把 GPU 的并行优势打掉。OWASP 目前的推荐顺序是:

  1. Argon2id(首选)
  2. scrypt
  3. bcrypt(老系统兼容)
  4. PBKDF2(合规要求时的下限)

如果项目是新的,直接用 Argon2id;已有 bcrypt 存量也不必急着迁移 —— 可以在用户下次登录成功时用新算法重算一遍,静默升级。

生成和验证

调试时想确认某个密码在 cost 12 下的哈希长什么样、或者验证一段 $2b$12$... 是不是对应某个密码,用 bcrypt 生成与校验,全程本地计算。

要给用户生成初始密码,用随机密码生成器,注意别用 Math.random() 自己造 —— 它不是密码学安全的随机源。

反过来:文件校验就该用 SHA-256

下载一个安装包,官网给了 SHA-256 校验值。你算完比对,一致就说明文件没在传输中被篡改。这里必须用快哈希:几 GB 的文件也要在几秒内算完。

用 文件哈希 可以在本地算 MD5 / SHA-1 / SHA-256 / SHA-512,文件不上传。文本内容用文本哈希。

判断标准很简单:数据要不要保密。要保密且不可逆 → 慢哈希;只求指纹相同 → 快哈希。

一张对照表收尾

场景 用什么 理由
用户密码 Argon2id / bcrypt(cost≥12) 必须慢,抗 GPU
文件完整性 SHA-256 必须快
内容去重、缓存键 MD5 也行(非安全场景) 快,碰撞风险无所谓
数字签名 SHA-256 + RSA/ECC/SM2 哈希只负责压缩,安全由签名保证
Base64 什么都不算 它是编码,不是哈希 —— 见这篇

记住这条分界线:哈希用于验证「是不是同一个东西」,加密用于「让别人看不到」。密码既不是加密也不是普通哈希,它是「故意很慢的哈希」。