发布于

Web 中的身份认证 2——密码认证

AI 辅助翻译自英文阅读英文原文

作者

@Author: Garfield Zhu

Web 应用身份认证(2)——密码认证

密码可能是我们接触最多的认证形式,也是最容易被搞砸的形式。听起来很简单——用户输入密码,服务器检查密码——但出错的方式很多,后果也非常严重。

下面来看看在服务器端处理密码时需要考虑的关键事项。

永远不要存储明文密码

这听起来显而易见,但现实中仍有人这样做。如果以明文存储密码,而数据库泄露,每个用户的密码都会立即暴露,攻击者甚至不需要取证。

而且人们会重复使用密码,你的网站泄露还可能危及他们在其他网站的账户,损害范围远不止你的网站。

规则很简单:永远不要存储用户实际输入的内容。存储一种可以验证密码、但无法还原密码的派生值。

哈希

基本做法是在存储前用单向函数处理密码。用户登录时,对输入内容进行哈希,再与已存储的哈希比较。

stored: hash(password)
verification: hash(input) == stored

使用 SHA-256 或 MD5 等通用哈希函数的问题在于,它们本来就是为快速计算而设计的。攻击者用 GPU 每秒可以计算数十亿个 SHA-256 哈希。如果攻击者拿到了哈希数据库,暴力破解就变得非常可行。

此外,MD5 和 SHA-1 目前在密码学上已经被攻破,不要把它们用于任何安全敏感的场景。

加盐

即使使用强哈希函数,仍有一个问题:两个用户使用相同密码时,会得到相同哈希。攻击者可以预先计算常见密码及其哈希(称为彩虹表),然后立即查找匹配的哈希。

解决办法是在哈希前为每个密码加入随机值——盐:

stored: salt + hash(salt + password)

盐按用户唯一,和哈希一起存储(它不是秘密,只是随机的区分值)。这样即使密码相同,两个用户存储的值也完全不同,预计算彩虹表就失去了作用。

现代密码哈希算法

正确的解决方案是使用专为密码设计的哈希函数,而不是通用哈希。它们被设计得缓慢且耗费内存,直接提高暴力破解的成本。

bcrypt

bcrypt 是经典选择。它会自动加盐,并提供可配置的成本因子来控制计算速度。硬件变快后,可以提高成本因子进行补偿。

$2b$12$LQv3c1yqBWVHxkd0LHAkCOYz6TtxMQJqhN8/LewYpfuA
^  ^  ^                                           ^
|  |  salt (22 chars)                             hash (31 chars)
|  cost factor (2^12 = 4096 iterations)
algorithm

成本因子 12 是目前合理的起点。成本为 12 的单次 bcrypt 大约需要 250 毫秒,对登录来说没问题,但对尝试数百万次猜测的攻击者来说非常糟糕。

scrypt 与 Argon2

scrypt 在 bcrypt 的时间成本之上增加了内存难度。攻击者不能只增加 GPU 核心,还必须按比例增加 RAM,这更难扩展。

Argon2 在 2015 年赢得密码哈希竞赛,是从零开始时当前推荐的算法。它有三个变体:

  • Argon2d——最大化抵抗 GPU 攻击
  • Argon2i——抵抗侧信道攻击
  • Argon2id——混合型,大多数场景推荐

大多数现代加密库都支持 Argon2id,条件允许就使用它。

密码策略

密码策略的目标是提高猜测用户密码的最低成本。真正重要的事项包括:

**长度胜过复杂度。**20 个字符的密码短语远强于由字母、数字和符号组成的 8 位密码。熵会随长度呈指数增长;最低 12 个字符是合理底线,8 个字符现在太短。

检查已知泄露密码。Have I Been Pwned 维护着来自已知泄露事件的数十亿个密码。专门拦截这些密码比任意复杂度规则更有效,因为攻击者会直接使用这些列表。

**不要强制定期更换。**强制定期轮换并不能真正提高安全性,只会让用户选择更弱的密码,如 Password1!Password2!NIST SP 800-63B 已删除轮换建议。

**已知泄露时必须更换。**如果检测到账户泄露或可疑活动,此时应强制修改密码。

暴力破解防护

即使哈希函数很慢,仍需防御在线暴力破解——攻击者直接反复访问登录端点。

速率限制

最简单的防御方式:限制单个 IP 或单个账户在时间窗口内的登录尝试次数。例如,每个账户每分钟 5 次失败尝试是合理起点。

但不要只依赖 IP 限制:攻击者会使用分布式僵尸网络,过于激进的 IP 限制还可能锁住位于 NAT 或代理后的合法用户。

账户锁定

连续失败 N 次后锁定账户,重置后才能解锁。这会完全阻断暴力破解,但也带来拒绝服务机会——攻击者只要知道用户名就能锁定任意账户。

更温和的办法是失败后逐步增加延迟:

  • 失败 1–3 次:不增加延迟
  • 失败 4–5 次:等待 5 秒
  • 失败 6 次以上:等待 30 秒,并要求 CAPTCHA

这样能减慢攻击者,又不会锁住真实用户。

CAPTCHA

对于高价值端点,在几次失败后增加 CAPTCHA 是合理的额外防护。但它会给合法用户带来困扰,因此不要在第一次尝试时就显示。

多因素认证

单靠密码并不理想——密码会被钓鱼、泄露和重复使用。加入第二因素后,窃取密码也不足以攻破账户。

常见的第二因素包括:

  • TOTP(基于时间的一次性密码)——典型的身份验证器应用流程,定义于 RFC 6238。服务器与用户设备共享密钥,并据此计算时间窗口内的验证码;验证无需网络调用。
  • 短信验证码——更易引导用户使用,但弱于 TOTP,因为短信可能遭到 SIM 换卡攻击拦截。
  • 硬件密钥(FIDO2/WebAuthn)——最强选项,设计上可抵抗钓鱼。

即使不强制要求,也值得支持 MFA;至少应提供 TOTP 选项。

参考