用户密码不能明文保存,也不应该使用 AES 等可逆算法直接加密后保存。正确做法是使用专用密码哈希算法,让服务器能够验证密码,却无法从数据库中还原原始密码。

结论先说:

新系统优先使用 Argon2id。无法使用时选择 scrypt;遗留系统可继续验证 bcrypt,并在用户登录成功后逐步迁移;必须满足 FIPS-140 时可使用 PBKDF2-HMAC-SHA-256。

密码为什么不能直接加密

加密是可逆的。系统要验证或恢复明文,就必须持有解密密钥。攻击者一旦同时拿到数据库和密钥,所有密码都会直接暴露。

密码认证并不需要恢复明文,只需要回答一个问题:用户这次输入的密码,是否和注册时相同?

因此,密码应使用单向哈希保存。

密码注册、验证与升级流程

注册时,服务端计算密码哈希并保存结果;登录时,根据记录中的算法和参数重新计算,再使用安全比较函数判断是否一致。

为什么 SHA-256 也不能直接存密码

MD5 和 SHA-1 已经不适合作为现代密码存储方案,但把它们换成单次 SHA-256 仍然不够。

SHA-256 的问题不是“已经被破解”,而是计算速度太快。它原本用于数据摘要、完整性校验和数字签名,快速计算是优点;密码数据库泄漏后,这个优点却会帮助攻击者使用 GPU 高速尝试候选密码。

密码哈希需要具备三个能力:

  • :每次猜测都要消耗明显的计算时间。

  • 可调成本:硬件变快后,可以提高迭代次数或内存占用。

  • 内存困难:增加 GPU、ASIC 大规模并行破解的成本。

密码哈希并不是让攻击者“无法计算”,而是让其每猜一次都付出更高代价。

当前应该选择什么算法

算法

建议

说明

Argon2id

新系统首选

同时消耗计算资源和内存,适合现代密码存储

scrypt

可用的替代方案

同样属于内存困难算法

bcrypt

主要用于遗留系统

生态成熟,但内存成本较低,并存在 72 字节输入限制

PBKDF2-HMAC-SHA-256

合规场景使用

兼容性强,常用于要求 FIPS-140 的系统

MD5 / SHA-1

禁止使用

速度快,并存在已知安全问题

单次 SHA-256 / SHA-512

不适合

通用哈希速度太快,容易被离线暴力破解

密码哈希算法选型

参考参数

OWASP 当前给出的最低参考配置为:

  • Argon2id:内存至少 19 MiB、迭代次数 2、并行度 1

  • scrypt:N = 2^17r = 8p = 1

  • bcrypt:cost 至少 10,并注意 72 字节输入限制。

  • PBKDF2-HMAC-SHA-256:至少 600,000 次迭代。

这些数字是参考基线,不是永远不变的固定答案。生产环境应进行基准测试,在不明显影响登录体验和系统稳定性的前提下,选择尽可能高的成本参数,并定期复查。

Salt、成本参数和 Pepper

一个合格的密码存储方案不仅包含算法名称,还包含 Salt 和成本参数。

Salt、成本参数与 Pepper 的关系

Salt

Salt 是每个密码独立生成的随机值。它的作用是:

  • 让两个相同密码产生不同哈希结果;

  • 阻止攻击者使用预先计算的彩虹表;

  • 避免一次计算同时匹配数据库中的大量用户。

Salt 不需要保密,可以和哈希结果一起存储。现代密码哈希库通常会自动生成和管理 Salt,不建议自行拼接字符串实现。

成本参数

Argon2id 的内存、迭代次数和并行度,bcrypt 的 cost,以及 PBKDF2 的迭代次数,都属于成本参数。

成本越高,正常登录和攻击者猜测密码都会变慢。参数过低没有足够防护,参数过高则可能拖慢服务,甚至放大拒绝服务风险,因此必须通过实际压测确定。

Pepper

Pepper 是可选的全局秘密值,用于增加纵深防御。它与 Salt 的区别是:

  • Salt 每个用户不同,可以公开;

  • Pepper 通常由系统共享,必须保密;

  • Pepper 不应存放在密码数据库中,应放在密钥管理系统、HSM 或受保护的执行环境中。

Pepper 不能代替 Salt,也不能弥补弱算法和弱密码。

数据库里实际保存什么

Argon2id 的完整记录通常类似:

$argon2id$v=19$m=65536,t=3,p=1$Salt$Hash

其中包含:

  • 算法和版本;

  • 内存、迭代次数、并行度;

  • Salt;

  • 最终哈希值。

这些信息可以公开保存,因为安全性不能依赖隐藏算法和参数。需要保密的是用户原始密码,以及可选的 Pepper 或服务器密钥。

数据库字段应预留足够长度。与其把 Salt、参数和哈希拆成多个字段,更常见的做法是直接保存密码库生成的完整编码字符串。

旧系统怎么迁移

不要尝试“解密”旧哈希,因为哈希本身不可逆。推荐采用登录时升级

  1. 保留旧算法的验证能力;

  2. 用户使用原密码成功登录;

  3. 检查当前算法或成本参数是否过时;

  4. 使用 Argon2id 重新计算;

  5. 用新记录替换旧哈希。

长期未登录的用户可以继续保留旧记录,或在风险较高时要求重置密码。

常见错误

  • 明文保存密码;

  • 使用 AES 加密密码,并把密钥放在同一套系统中;

  • 使用 MD5、SHA-1 或单次 SHA-256;

  • 所有用户共用同一个 Salt;

  • 自己设计“多轮哈希”或自创密码算法;

  • 把 Pepper 和密码表存放在同一数据库;

  • 忽略 bcrypt 的 72 字节限制;

  • 多年不调整成本参数;

  • 在日志、异常信息或监控系统中输出密码;

  • 在客户端先哈希,然后把哈希值当成可直接登录的密码。

客户端哈希不能替代 HTTPS 和服务端密码哈希。如果客户端哈希值可以直接用于登录,它本身就成了新的密码,攻击者拿到后仍然可以重放。

最后的选择

普通业务系统可以直接遵循这套规则:

  • 新系统:Argon2id

  • 不支持 Argon2id:scrypt

  • 已使用 bcrypt:继续验证,并逐步迁移;

  • 需要 FIPS-140:PBKDF2-HMAC-SHA-256

  • MD5、SHA-1、单次 SHA-256:尽快淘汰。

密码哈希主要降低数据库泄漏后的离线破解风险。完整的认证系统还需要 HTTPS、登录限流、泄漏密码拦截、多因素认证、日志脱敏和安全的密码重置流程。

参考资料

  1. OWASP Password Storage Cheat Sheet
    https://cheatsheetseries.owasp.org/cheatsheets/Password_Storage_Cheat_Sheet.html

  2. RFC 9106:Argon2 Memory-Hard Function
    https://www.rfc-editor.org/rfc/rfc9106.html

  3. NIST SP 800-63B:Authentication and Lifecycle Management
    https://pages.nist.gov/800-63-4/sp800-63b.html