「用户的密码,到底该怎么存进数据库才对?」——这是每个做服务的人都必然会撞上一次的问题。答案很明确。我们不涉及攻击手法,只按顺序讲清楚安全的存储方法。
为什么明文、加密、裸哈希都不行
我们按数据库迟早会泄露这个前提来考虑。一旦泄露,不同的存储方式造成的损失天差地别。
- 明文:泄露的瞬间所有密码一览无余。再加上重复使用同一密码,用户在其他服务上的账号也会被连锁攻陷。最糟。
- 加密(可逆):有密钥就能还原=密钥一起泄露就等同于明文。密码本就没有读回来的必要,因此可逆这件事本身毫无意义。
- 裸哈希(MD5/SHA-256):快速哈希让攻击者能高速尝试海量候选。常见密码会被彩虹表或暴力破解还原出来。
走到安全为止的“四个阶段”
把它理解成给弱方式一步步叠加对策的过程,会很快上手。
要点在于,盐和慢哈希的职责是不同的。盐负责让「预计算(彩虹表)和批量破解重复密码」失效。慢哈希负责把「暴力破解的速度压到不现实的水平」。两者齐备,才第一次称得上安全(→ 什么是哈希化)。
实践:现在就该做的事
使用 Argon2id(或 bcrypt)
新项目把 Argon2id 作为首选。它还能要求消耗内存,因此对 GPU 暴力破解很有抵抗力。如果更看重成熟稳定的实现,bcrypt 在实践中也完全够用。两者都直接使用标准库的实现。
盐交给“自动”去做
bcrypt/Argon2 会为每个用户自动生成盐,并把它一并塞进哈希字符串里。你不需要自己另存一列盐。手动去拼 MD5(salt + password),正是不该犯的典型错误。
按运行环境设置成本(强度)
把 bcrypt 的成本系数、Argon2 的内存・迭代・并行度,设到正常登录在体感上察觉不到延迟的范围内的最大值。服务器越快,攻击者也越快,所以要以年为周期复核并上调。
校验用标准的比对函数
登录时,把已存哈希与输入用库提供的比对函数进行匹配(很多实现已经做成了对时序差有所防范的常数时间比较)。不要自己写字符串相等判断。
弱哈希用“登录时重新哈希”来迁移
如果已经用 MD5/SHA-256 存了,就在用户登录成功的那一刻用新方式重新哈希并覆盖保存。对于尚未登录的部分,可以用 bcrypt 等把既有哈希再包一层,用这种双重哈希暂时提升整体强度。
常见的错误做法 vs 正确的实现
常见错误
- 把密码加密后存储(密钥一泄露就全完)
- 直接存
MD5(password)或SHA-256(password) - 所有用户共用同一个盐
- 自己手动拼
hash(salt + password)
正确的实现
- 用 Argon2id / bcrypt 做单向哈希
- 盐按每个用户分配(由库自动附加)
- 成本设在体感可接受范围内的最大值,并定期上调
- 直接使用标准的哈希/比对函数
本站的观点:密码存储是该踩在“成熟正解”上的地方
密码存储不是一个可以彰显独创性的领域。老老实实踩在经过全球验证的 Argon2/bcrypt 标准实现上,既最安全,运维也最省心。本站的立场是「在这里不要搞创意发挥」。真正该投入精力的,是从根本上减少对密码的依赖——把多因素认证(MFA)做到位,以及未来向通行密钥(无密码)迁移。再强的哈希,也兜不住弱密码和重复使用。
延伸阅读
- 术语:什么是密码哈希化 / 什么是盐
- 入门:密码的正确保管(用户侧) / 如何挑选密码管理器
- 对策:如何挑选两步验证(MFA)(减少对密码的依赖)
- 术语:什么是通行密钥(向无密码迁移)
FAQ
Q密码加密后再存储不就行了吗?
不行,加密并不合适。加密只要有密钥就能还原(解密),密钥一旦泄露,所有密码就会被还原成明文。密码对运营方来说也没有读回来的必要,因此无法还原的『哈希化』才是正解。不过裸哈希还不够,需要再配合为每个用户加盐以及慢哈希(bcrypt/Argon2)。
Qbcrypt 和 Argon2,到底该用哪个?
如果是新项目,Argon2(尤其是 Argon2id)是首选。因为它的内存占用和计算量都可调,能很好地抵御 GPU 暴力破解。如果更看重既有资产或成熟稳定的实现,bcrypt 在实践中也完全够用。关键在于:无论选哪个,都要『直接使用标准库的实现,绝不自己拼装』。
Q我已经用 MD5 或 SHA-256 存了密码,该怎么迁移?
没法把它们一次性还原成明文(这恰恰是它的优点),所以要在登录时迁移。当用户成功登录的那一刻,用新方式(如 Argon2id)对输入的正确密码重新哈希并覆盖保存。对于一直没登录的用户,还可以用 bcrypt 等把既有哈希『再包一层』,用这种双重哈希暂时提升一下整体强度。