盐与慢哈希

一次真实的教训

账号被盗事件后,我做的第一件事是检查自己的密码存储。

结果发现:我把用户的密码明文存进了数据库。

当时我想的是「系统小,用户少,不会有人来打」。直到那天我看了安全论坛的报道——每年都有大量网站因为明文存密码被拖库,用户的密码被拿去撞库、洗库,最后变成骗子手里的「密码字典」。

明文密码是认证系统里最不能碰的雷。今天我们从头把它做对。

为什么不能只用哈希

有些人会说:不存明文,存哈希不就行了?

只做一次普通哈希(比如 MD5、SHA-256)确实比明文好,但远远不够:

  1. GPU 暴力破解:MD5、SHA 这类快速哈希,用 GPU 每秒能算几十亿次。一个 8 位数字密码可能几分钟就被跑完。
  2. 彩虹表:预先把常见密码的哈希算好存起来,拿库里的哈希一比对就能反推出明文。
  3. 相同密码相同哈希:两个用户都用 hunter2,哈希一模一样,攻击者一次破解,两个账号一起沦陷。

所以正确做法是两条腿走路:加盐(salt)+ 慢哈希(slow hash)。

加盐:让相同密码产生不同哈希

盐是一段随机字符串,在哈希之前拼到密码里:

password + salt -> hash

每个用户(甚至每次改密)都用独立的随机盐。这样:

  • 相同密码 → 不同哈希,彩虹表失效。
  • 攻击者必须对每个用户单独爆破,成本大幅上升。

慢哈希:让每次尝试都很贵

MD5、SHA 的问题是算得太快。慢哈希算法通过大量迭代(和内存消耗)把单次计算拉慢到毫秒甚至百毫秒级:

算法特点适用
bcrypt经典慢哈希,可调 cost,抗 GPU最广泛
scrypt额外消耗内存,抗专用硬件对硬件攻击更强
Argon2现代设计,同时抗 GPU 与内存侧信道新系统首选

用户登录是一次,慢一点没关系;攻击者爆破是几亿次,慢一点点就是天文数字的成本差距。

亲自动手看看

下面是一个(示意性的)密码哈希过程,你可以输入一个密码,观察盐和慢哈希如何配合:

密码存储:盐 + 慢哈希

明文密码(空)绝不落库
随机盐 salt每个密码独立
password_hash(bcrypt 示意)(等待输入)仅此值入库
即使数据库泄露,攻击者拿到的也只是 password_hash 和 salt。慢哈希与盐会显著提高批量撞库成本。

数据库里存什么

设计上,用户表只需要保存:

user_account
├── id
├── email / phone
├── status
└── password_hash   <- 算法版本 + 盐 + 哈希值

推荐把算法版本一起存进去,例如 $2b$12$... 里的 2b 是 bcrypt 版本、12 是 cost 参数。这样将来算法升级时,可以渐进式迁移:用户登录成功后,如果检测到旧算法,就顺手用新算法重新哈希,不用一次性强制全量重置。

上线检查清单

  • 密码是否使用 bcrypt / scrypt / Argon2 等慢哈希 + 独立盐?
  • 数据库是否只保存 password_hash,绝不保存明文或可逆加密?
  • 是否保存算法版本号,支持渐进式升级?
  • 日志、报错、接口响应里是否有可能泄露密码或哈希?

把密码这关守住,认证系统才算有了像样的地基。下一节看登录流程本身。

发现内容有问题?

告诉我们哪里需要改进。