密码安全基础:哈希算法与密码存储
聊密码安全,绕不开哈希。说白了,哈希就是“不可逆的指纹”。你给它任意长度的数据,它吐出一段固定长度的摘要,而且你没法从摘要反推出原始数据。这个特性,让它成了存储密码的首选方案。
但别急着高兴——不是所有哈希都适合存密码。我见过不少项目,直接拿SHA-256存用户密码,以为这样就安全了。结果?嗯,后面的故事不太好看。
SHA-256:计算快,但存密码是灾难
SHA-256是SHA-2家族的一员,输出256位(32字节)摘要。它的设计目标是防篡改和防碰撞,不是防暴力破解。恰恰相反,SHA-256太快了——现代GPU一秒能算几十亿次。你想想看,如果数据库泄露,攻击者用跑字典的速度几乎是实时的。
那SHA-256就完全没用吗?当然不是。文件校验、数字签名、区块链都靠它。但存密码?千万别。
加盐:让你的密码与众不同
为什么需要加盐?你看,两个用户如果密码相同,没加盐时哈希值也一样。攻击者只要破解一次,就能知道所有同密码的用户。加盐就是在密码后面贴一段随机字符串(盐),再算哈希。这样,即使密码相同,哈希值也不同。
我个人的习惯:盐至少16字节,用密码学安全的随机数生成器(比如 os.urandom 或 SecureRandom),每个用户独立的盐。盐本身不需要保密,可以明文存在数据库里。
加盐的注意事项
- 盐必须随机且足够长:不要用用户名、时间戳做盐,太容易预测。
- 每个用户/每次密码重置都要重新生成盐:我用过一个库,它重复使用了固定的盐……简直是灾难。
- 加盐后仍需慢哈希:加盐只解决彩虹表问题,不解决暴力破解的速度问题。SHA-256+盐依然快,攻击者照样可以每秒跑上亿次。
BCrypt vs Argon2:慢哈希的典范
慢哈希,就是故意让计算变慢。通过增加迭代次数、内存消耗,让单次验证耗时可控(比如0.1秒),但攻击者想大规模跑字典就变得极其昂贵。
目前最主流的两个慢哈希:BCrypt 和 Argon2。直接对比表格更直观。
| 特性 | BCrypt | Argon2 |
|---|---|---|
| 出生年份 | 1999 | 2015(PHC冠军) |
| 可调参数 | 工作因子(cost,即迭代次数) | 时间成本、内存成本、并行度 |
| 抗GPU/ASIC | 中等(主要是CPU bound) | 强(内存硬,GPU没法作弊) |
| 输出格式 | $2b$10$...(自带盐和版本) |
$argon2id$v=19$... |
| 密码长度限制 | 72字节(超过截断) | 无硬性限制 |
| 推荐场景 | 传统Web应用,兼容性好 | 新系统、高性能环境、对抗硬件攻击 |
我在实际项目中两个都用过。BCrypt的最大优点是生态老、库多、坑少。几乎所有语言都有成熟实现,默认配置就够用。但要注意它的密码长度上限——72字节。如果用户输入超长密码(比如 >72字符),会被悄悄截断,导致后来永远验证失败。我遇到过这类工单,排查了半天才发现是BCrypt的锅。
Argon2是2015年密码哈希竞赛的冠军,它有三种变体:Argon2d(抗侧信道)、Argon2i(抗时间攻击)、Argon2id(混合)。我建议直接用Argon2id,它兼顾了安全性和抗攻击性。它的参数可以独立控制时间、内存、并行度,灵活性比BCrypt高一个量级。
密码哈希的完整流程图
下面这张图展示了我推荐的核心流程:
选型建议与总结
如果你还在犹豫,我直接给结论:
- 不要用 SHA-256 / MD5 存密码,哪怕加了盐。
- 优先用 Argon2id,参数设成
时间成本=3, 内存=64MB, 并行度=4(或适配你的运行环境,保证单次验证 ≤ 0.5秒)。 - 如果兼容性优先(比如Java Spring Security默认支持BCrypt),用BCrypt,cost至少设为10,最好12。
- 一定要加盐,盐由安全随机数生成,长度16字节以上。
- 密码长度限制:BCrypt注意72字节截断,建议在前端主动截断或提醒;Argon2无此问题。
总结下来,密码存储安全不在于算法多炫,而在于你是否用了慢+盐+抗硬件的组合。记住一句话:让攻击者算一个密码的成本,大于他放弃的成本。 这就是BCrypt和Argon2的价值。