约 7 分钟读完 · 更新于 2026-10-21 · 文中每条都能在下面的配套工具里动手验证
密码存 MD5、SHA-1、单次 SHA-256,甚至「SHA-256 加一个固定后缀」,在拿到数据库的人面前和明文差别不大——彩虹表能直接查,GPU 每秒能试几十亿次。正确方向只有一个:bcrypt / scrypt / Argon2id 这类刻意设计得慢的哈希,每个密码配独立随机盐,把耗时参数(cost)写进存储格式里,以便以后调高而不动数据结构。
| 算法 / 方式 | 典型用途 | 能存密码吗 | 原因 |
|---|---|---|---|
| MD5 | 老系统文件校验、遗留 ETag | 不能 | 太快且无盐,彩虹表直接查 |
| SHA-1 / SHA-256 / SHA-512 | 文件完整性、签名摘要、去重键 | 不能 | 设计目标是抗碰撞,不是抗爆破 |
| 单次 SHA-256 + 固定盐 | 部分遗留系统的过渡方案 | 不建议新用 | 只挡彩虹表,挡不住 GPU;还要自建升级路径 |
| PBKDF2-HMAC-SHA256 | FIPS 约束场景、老 iOS 方案 | 可以 | 靠迭代次数控成本,内存占用低但同样可被 ASIC 打 |
| scrypt | 文件加密口令派生、备份密钥 | 可以 | 内存硬,显著削弱 GPU / ASIC 并行优势 |
| bcrypt | Web 主流:Spring / Laravel / Rails | 推荐 | cost 可调、盐内置在哈希串里 |
| Argon2id | 新项目首选 | 推荐 | 同时抗 GPU 与旁路攻击,参数更细 |
| AES 可逆加密 + 固定口令 | 必须读回明文的业务字段 | 不用于密码 | 密钥一旦泄露等于交出明文 |
| Base64 / 各类编码 | 传输层表示、URL 安全 | 不能 | 无密钥、随时可逆,不是保护手段 |
salt 是每个密码单独生成的随机串(至少 16 字节),作用是让相同密码的哈希值各不相同,彩虹表因此彻底失效。但它挡不住另一种攻击:攻击者拿到哈希后离线猜。哈希函数越快,猜得越快——单张高端 GPU 每秒几十亿次 SHA-256,纯 8 位数字密码的一亿种组合几秒就穷举完了。补上这一段要靠慢哈希:bcrypt 的 cost 每加 1 耗时翻倍,Argon2 直接调内存和迭代次数。目标不是让哈希"看着乱",而是让攻击者的单位成本高到不划算。
const row = db.query("SELECT pw_md5 FROM users WHERE email = ?", [email]);
if (row.pw_md5 === md5(password)) { /* 不该走到这里 */ }
// 同样危险:所有用户共用一个固定盐,等于没加盐
const bad = sha256(password + "2024salt");const bcrypt = require("bcrypt");
// cost=12:同一台机上单次校验约 200-400ms,按自己的 P99 预算调
const hash = await bcrypt.hash(password, 12);
// -> "$2b$12$LQv3c1yqBWVHxkd0LHAkCOYz6TtxMQJqhN8/LewYfCz7c5X1MSky"
const ok = await bcrypt.compare(password, hash); // 唯一正确的校验方式const argon2 = require("argon2");
const argonHash = await argon2.hash(password, {
type: argon2.argon2id,
memoryCost: 19456, // KiB,约 19MiB
timeCost: 2,
parallelism: 1,
});
// 库名、版本、参数、随机盐全部随哈希一起返回,无需额外字段const crypto = require("crypto");
function safeEqualHex(a, b) {
const ba = Buffer.from(a, "hex"), bb = Buffer.from(b, "hex");
return ba.length === bb.length && crypto.timingSafeEqual(ba, bb);
}
// 直接 a === b 会在第一个不同的字节处提前返回,理论上可被逐字节试探| bcrypt cost | 单次校验耗时(参考) | 含义与适用 |
|---|---|---|
| 10 | 50-100ms | 下限区间,只适合内部系统或已有高硬件预算 |
| 12 | 200-400ms | 多数 Web 后端的起点,按 P99 体验调整 |
| 14 | 1-2s | 高价值账户、管理员通道,注意别挡在同步请求里 |
| 6-8 | < 30ms | 不要用来存密码,只适合短期令牌、缓存键 |
先算清泄露面,再止血。第一步确认范围:dump 出来的表里有没有明文列——很多老系统同时留着 password 和 pw_hash 两列,只看哈希列会漏。第二步按存储方式分支:明文或无盐哈希视同全部已破,立刻全站强制改密、失效所有会话、关闭邮件找回通道(攻击者会比你先改邮箱)。第三步若存的是加盐哈希,不要一上来就全员改密:先加 pepper、把 cost 提到当前标准,让攻击者的离线批量破解成本抬高,再分批改密,避免整改动作本身把服务打挂。第四步补配套:MFA、登录失败限流、异常 IP 与新设备登录告警。第五步把密码存储与升级策略写进代码规范和安全评审清单,不要只在复盘纪要里写一句「以后注意」。
「必须含大小写 + 数字 + 符号」这类规则只会把用户推向 P@ssw0rd1 这种可预测组合。评估强度看三件事:长度(12 位以上是当前性价比最高的手段)、熵值(字符集大小与长度共同决定随机性)、以及是否落在常见密码字典里。熵低不代表这个密码一定被秒破,但字典攻击的成本几乎只取决于它在字典里的位置。注册和改密时先做一次本地强度评估,比事后靠规则打回重填的转化率好得多。
可以存,但它挡不住任何人。MD5 太快且无盐,拿到库的人用彩虹表或 GPU 爆破几秒就能还原。密码只应该用慢哈希:bcrypt、scrypt 或 Argon2id,每个密码配独立随机盐。核对一条已有哈希能否匹配明文,用对应库自带的校验函数即可。
salt 跟密码一起存在数据库里,作用是让相同密码的哈希不同、彩虹表失效;pepper 不进数据库,放在 KMS 或环境变量里,作用是让脱库的哈希无法离线破解。pepper 补的正是 salt 补不了的那一半——盐和哈希一起泄露时,salt 就没意义了。代价是轮换 pepper 会要求全站重新哈希甚至强制重登。
没有标准答案,以「单次校验 100-250ms」为靶子压测定出来:bcrypt 从 10 起,12 是多数 Web 后端的起点,14 以上留给高价值账户。cost 每加 1 耗时翻倍,所以把它做成配置项而不是写死常量,登录时发现存量哈希参数低于当前值就顺手重哈希,以后调高就不必等用户全部改密。
不用通知任何人改密。登录时同时支持新旧两套算法,先用旧算法校验,通过后立刻用 bcrypt 重新哈希并覆盖原字段,再设一个截止日期把旧分支删掉。想先摸清库里还有多少条老格式,抽样跑一遍登录校验日志即可。