直接哈希口令的三个问题
- 速度太快:SHA-256 每秒可算上亿次,攻击者可高速穷举;
- 无盐 → 彩虹表:相同口令得到相同摘要,一张预计算表就能批量还原常见口令;
- 泄露即连坐:两个用户口令相同,摘要也相同,一眼就能看出来。
加盐解决什么
盐是每个用户独立的随机值,与口令拼接后再哈希,并与摘要一起存储。它让彩虹表失效(每个盐都要单独预计算),也让相同口令产生不同摘要。注意:盐只防“批量预计算”,不防“单个口令的暴力穷举”——那是下一层要解决的。
慢哈希解决什么
bcrypt、scrypt、Argon2 通过可调工作因子刻意拖慢计算,把每秒可尝试次数从十亿级压到几千级。对正常登录几乎无感,对攻击者则是数量级的成本提升。
| 方案 | 是否适合存口令 | 说明 |
|---|---|---|
| MD5 / SHA-1 | 绝对不适合 | 已被证明可碰撞,速度过快 |
| SHA-256(裸哈希) | 不适合 | 仅用于校验完整性,不用于口令 |
| bcrypt | 推荐 | 自带盐,工作因子可调 |
| Argon2id | 推荐(首选) | 抗 GPU / ASIC,参数可调内存与时间 |
| PBKDF2 | 可用 | 合规场景常见,抗 GPU 能力弱于 Argon2 |
参数怎么定
- 目标是把单次校验耗时控制在 50–200ms(服务端可接受的上限);
- bcrypt 的 cost 从 10 起,逐年按硬件提升上调;
- Argon2id 常用起点:内存 19–64MB、迭代 2–4 次、并行度 1–2,再按上面耗时目标微调;
- 参数要与摘要一起存储,方便日后平滑升级。
常见问题
盐要保密吗?不需要,盐的作用是唯一而非保密,但必须每个用户不同、足够随机。能用固定盐吗?等于没有盐。加密存储口令行不行?不行——密钥一旦泄露,所有口令可被直接解密;哈希的单向性正是优势。
旧系统如何平滑迁移
- 不要直接改密码字段:先新增算法标识、盐值与哈希字段(如 algo、salt、hash);
- 登录时双轨校验:老用户仍用旧算法校验,成功后立即用新算法重算并写回;
- 惰性迁移:长期不活跃的用户可在下次登录时再迁移,或到期后强制重置;
- 清理旧值:迁移完成后删除旧哈希字段,并记录迁移比例便于核对。
还要做对的几件事
- 限流与渐进锁定:对同一账号与同一 IP 的登录尝试做限制,防止在线爆破;
- 日志不记录明文口令:日志、埋点与错误上报都要过滤口令字段;
- 重置链接短时效且一次性:建议 15–30 分钟,使用后立即失效;
- 别自己实现算法参数:使用成熟库与推荐参数,定期跟随官方建议上调工作因子。
实战案例:三种典型数据泄露复盘
- 明文 / 可逆存储:库被拖走后全体口令直接泄漏——最严重,攻击者可立即登录。
- 无盐 SHA-256:相同口令摘要相同,攻击者用彩虹表批量还原常见口令。
- 有盐但单轮:盐挡住了彩虹表,却挡不住每秒上亿次的暴力枚举——在 GPU 上仍是“秒破”。
常见问题(FAQ)
bcrypt / Argon2 参数怎么定?以“单次校验约 100–250ms”为目标,据此调 cost/rounds 与内存,而不是照抄固定数字。能先 SHA-256 再 bcrypt 吗?现代实现可以(bcrypt 有 72 字节限制),但多数场景直接用 Argon2 更简单。盐要单独存一列吗?不必,bcrypt/Argon2 的哈希串自带盐,整串保存即可。换算法时老用户怎么办?登录成功时用新算法重算并覆盖,平滑迁移。
算法参数对比
- bcrypt:经典选择,成本因子每加 1 耗时翻倍,常用 10–12;输入上限 72 字节,超长口令需先预哈希;
- scrypt:内存与 CPU 双重开销,抗 GPU 更强,参数为 N、r、p;
- Argon2:目前推荐首选,可独立调节时间、内存与并行度,
Argon2id兼顾抗侧信道与抗 GPU; - PBKDF2:标准兼容性最好(FIPS 场景常用),但抗 GPU 能力弱于 scrypt / Argon2。