什么是安全中的加盐?密码哈希详解

什么是安全中的加盐?密码哈希详解

2012年6月,一名攻击者将1.17亿条LinkedIn账户记录泄露到一个俄罗斯论坛上。这份列表实际上是由大量未加盐的SHA-1哈希值组成。SHA-1算法速度很快。由于没有盐值来区分相同的密码,相同的哈希值会重复出现数千次。大约72小时内,安全研究人员就破解了该文件的90%左右。2016年5月,当这起泄露事件再次浮出水面,成为一个包含1.67亿条记录的数据集时,这些凭证被用于随后数年的撞库攻击。

早在1979年,就有一种防御措施可以将这次泄露事件的影响降到最低——即在所有存储的密码中添加盐值。这种方法叫做加盐。它既不新鲜,也不昂贵,更谈不上特别高明。但它却决定了数据库泄露事件是被控制在一定范围内,还是会成为每个人长达五年的密码难题。

大多数现代密码存储故障仍然源于同一个根本原因:有人认为“我们使用 SHA-256”就足够了。本文将详细介绍什么是加盐,它在实践中是如何工作的,它能保护哪些安全漏洞以及不能保护哪些安全漏洞,并介绍 OWASP 建议的 2026 年默认加盐方案。

什么是安全和密码哈希中的加盐?

把这个话题简化成一句话。加盐是指在密码哈希处理之前,向密码添加随机数据。这个随机值(即盐值)在每个账户上生成一次,与新密码哈希值一起生成,并以明文形式存储在生成的哈希值旁边,每次登录时都会读取。它不是密钥,不是秘密,也不是加密。它是一个公开的修饰符,只有一个作用:确保即使两个用户拥有相同的密码,他们存储的哈希密码也永远不会相同。

哈希本身是单向的。将密码输入 SHA-256 或 Argon2id 算法,你会得到一个固定长度的值,任何算法都无法逆向推导。但问题在于,“任何算法”都无法逆向推导——只需在预先计算好的字典中查找哈希值即可。加盐可以堵住这个捷径。在哈希过程中添加盐值,即使密码文本完全相同,也能为每个用户生成唯一的哈希值。像“password”和“qwerty”这样的常见密码在数据库中不再会相互冲突。加盐确保即使泄露一行数据,攻击者也无法获取任何其他用户的信息。

区分有效盐值和无效盐值的三条规则。第一条规则:每个密码都有其专属的随机盐值,该盐值在用户注册或重置密码时生成。跨账户重复使用的加盐密码与未加盐密码相比几乎没有任何优势——批量攻击的原理相同。第二条规则:盐值可以公开。仅凭盐值本身并不能为攻击者提供任何破解底层哈希函数的捷径。因此,盐值与哈希值一起存储在数据库中,这种做法是合理的,甚至值得提倡。第三条规则:盐值必须来自加密安全的伪随机数生成器。Linux 通过 `getrandom(2)` 函数提供伪随机数生成器。Node 将其称为 `crypto.randomBytes`。Python 标准库将其封装在 `secrets.token_bytes` 中。非加密的 `Math.random()` 函数并不适用,尽管它在实际审计代码中的出现频率远高于应有的水平。

两个人选择“hunter2”作为密码,应该得到两个完全不同的哈希值。加盐正是实现这一点的关键。如果没有加盐,数据库不仅会泄露哈希值,还会泄露密码共享者的社交关系图。

密码加盐的工作原理是什么?

该机制分为四个步骤,十年来的密码存储漏洞事件表明,几乎每一次失败都是因为有人跳过了其中一个步骤。

第一步在注册时运行。应用程序会向密码学安全伪随机数生成器 (CSPRNG) 请求 16 到 32 个随机字节。这就是盐值。OWASP 2025 指南将 16 字节(128 位)作为最低标准;auth0 和一些密码管理器厂商建议使用 32 字节。NIST SP 800-63B-4 规定了 4 字节的最小值,但警告说,当用户数量达到 6.5 万左右时,生日相关的密码冲突就会开始出现,这也是为什么实际上没有人使用 4 字节的原因。

第二步将盐值与用户选择的密码结合。最常见的形式是字符串拼接,盐值可以放在密码字符串之前或之后。有些系统则使用 HMAC,将盐值作为 HMAC 密钥,这样可以避免原始字符串拼接中一些难以处理的长度扩展问题。

第三步是将组合后的输入输入通过密码哈希算法进行处理——密码学教科书称之为密钥派生函数(KDF)。大多数团队过去常常在这一步犯错,他们通常会选择通用的哈希算法,然后就认为万事大吉了。普通的 SHA-256 本身并不适合用于存储密码。到 2026 年,消费级 GPU 每秒可以处理超过一千亿次 SHA-256 哈希运算,因此即使使用盐值,每次猜测的加密哈希成本仍然微不足道。加盐可以阻止彩虹表攻击,但它本身并不能减缓暴力破解密码的速度。这里合适的工具是故意降低速度、占用大量内存的函数:Argon2id 是 OWASP 推荐的默认算法,scrypt 和 bcrypt 也可接受,而当 FIPS-140 标准要求必须选择时,可以使用迭代次数非常高的 PBKDF2。

第四步会同时存储哈希值和盐值。现代库会处理存储格式,因此您无需手动管理存储的盐值。bcrypt 的输出类似于 `$2b$12$9f4c8a7b...kQR8YZpL9`——这个字符串包含了算法标识符、成本因子、盐值和哈希值。Argon2id 使用类似的 PHC 字符串格式,即 `$argon2id$v=19$m=19456,t=2,p=1$$`。您只需将该字符串写入数据库列即可。该库已经完成了安全存储密码所需的一切工作——生成随机数据作为盐值,使用慢速哈希算法对密码进行哈希运算,并将结果打包以便检索。

登录流程与此流程相反。应用程序会查找用户的盐值和存储的哈希值,然后使用相同的盐值通过相同的算法处理提交的密码,并将结果与存储的哈希值进行比较,比较过程使用恒定时间。恒定时间至关重要:简单的 `==` 比较会泄露匹配的字节数信息,这曾导致过实际的计时攻击漏洞。请使用 `hmac.compare_digest`、`crypto.timingSafeEqual` 或您框架的等效方法。

安全中的盐

为什么加盐很重要:彩虹桌问题

彩虹表正是加盐技术发明之初就为了抵御的那种攻击。想象一下,一个预先计算好的查找表,它将所有可能的密码——包括字典中的每个单词、常见变体以及泄露列表中的条目——都映射到其对应的 SHA-256 哈希值。这些查找表确实存在,它们在破解论坛上出售,而且规模高达 TB 级。一旦攻击者掌握了未加盐哈希值的数据库转储,在彩虹表中查找哈希值就变成了数据库查询,而不是破解工作。2012 年 LinkedIn 密码破解事件正是如此:攻击者使用彩虹表对 1.17 亿个未加盐的 SHA-1 哈希值进行破解,其中 90% 在短短三天内就被破解。

如果为每个用户添加一个盐值,彩虹表就无法正常工作了。攻击者需要为每个唯一的盐值预先计算一个单独的表。如果使用 16 字节的随机盐值,这意味着 1.17 亿用户需要 1.17 亿个不同的表——而且每个表的大小仍然会达到 TB 级。仅存储成本就足以证明这种做法不可行。因此,该攻击从威胁模型中剔除。

这就是加盐的全部作用。它的作用范围很窄,这是有意为之。加盐并不能减缓针对特定用户的蓄意暴力破解尝试。加盐也无法阻止利用先前泄露的凭据进行撞库攻击。如果密码本身是“123456”,加盐也无济于事——攻击者会尝试显而易见的字典,每次猜测都会重新计算盐值,但并不会显著改变每次猜测的成本。

加盐可以有效缓解以下问题:彩虹表攻击,以及同一数据库中不同用户密码重用信息泄露。慢速哈希可以缓解以下问题:每次猜测的成本。胡椒粉可以缓解以下问题:仅数据库窃取。多因素身份验证 (MFA) 可以缓解以下问题:撞库攻击。加盐是安全堆栈中的一层——通过添加盐值来增加密码破解难度,并不等同于让每个密码都难以猜测。加盐的目标是为每个用户生成不同的哈希值;而增加每次猜测的成本则是另一个问题。

哈希、加密和加盐:

这三个术语经常被混淆,尤其是在非专业人士的文章中。它们不能互换使用。

加密哈希
可逆的是的,用钥匙不,这是有意为之。修饰符,而非独立存在
输出长度变量,与输入匹配固定(通常为 256 位)不适用 — 更改哈希输入
用于数据保密性完整性,密码存储加强哈希值
所需钥匙是的,秘密不,用的是随机盐

加密可以保护你以后想要检索的数据。接收方持有密钥,应用加密后即可读取原始数据。哈希是单向的:一旦密码被转换成哈希值,任何算法都无法逆向还原。加盐是应用于哈希函数输入的一种修饰符——它本身没有任何意义。

Adobe 2013 年的数据泄露事件就是一个值得警惕的例子。Adobe 使用 3DES 算法在 ECB 模式下加密密码,并使用一个全站通用的密钥来存储 1.53 亿条用户记录。这种做法犯了三重错误。首先,加密本身就不是存储密码的正确方法。其次,ECB 模式会将相同的输入直接转换为相同的输出。最后,在整个数据库中重复使用同一个密钥意味着,只要解密一次,所有 1.53 亿条记录都会被解密。Schneier 称之为历史上最严重的密码泄露事件之一。最终的解决方案是,在每一层都切换到加盐的慢速哈希算法。

盐与胡椒:双层防御

Pepper 是 Salt 的鲜为人知的“表亲”。其概念是:为应用程序处理的每个密码添加一个额外的秘密值,但该值存储在数据库之外。它可以是环境变量、密钥管理服务条目或驻留在硬件安全模块 (HSM) 中的密钥。Salt 以明文形式与哈希值一起存储,而 Pepper 则不然。

威胁模型是仅数据库窃取。如果攻击者仅窃取了数据库(通过 SQL 注入、配置错误的备份或泄露的转储文件),他们就拥有了盐值和哈希值,但缺少密钥。没有密钥,他们甚至无法开始暴力破解,因为他们每次尝试哈希哈希都会缺少全局密钥修饰符。

典型的实现方式是运行 `argon2id(salt + password + pepper)`,或者更谨慎的做法是,先计算 `HMAC(pepper, password + salt)`,然后将结果传递给 Argon2id。OWASP 建议在高价值系统中使用 pepper,但也承认其缺点:pepper 的轮换操作非常繁琐。轮换意味着每次用户登录时都要重新哈希其密码,而且如果 pepper 丢失且没有备份,则所有账户都将无法验证。大多数消费者应用程序会跳过 pepper 的使用。但银行、政府和医疗保健等行业的系统通常不会。

2026 年的现代密码哈希技术

OWASP 2025 年密码存储指南对四种算法进行了优先级排序。所有这些算法都内置了 Salt 值,无需手动生成。

算法OWASP 2025 最低要求规格
Argon2id(首选) m=19 MiB,t=2,p=1 RFC 9106 (2021)
scrypt N=2^17,r=8,p=1 RFC 7914 (2016)
bcrypt(旧版)成本≥10;72字节输入容量尼尔斯·普罗沃斯,1999年
PBKDF2-HMAC-SHA256 60万次迭代RFC 2898;仅限 FIPS 规范

Argon2id 是一种内存密集型攻击,这意味着每次哈希猜测不仅消耗 CPU 周期,还会占用一定大小的内存。OWASP 规定每次哈希至少需要 19 MiB 内存,这意味着攻击者在构建定制 ASIC 时,还必须构建大量的快速内存,而内存是任何攻击设备中最昂贵的部分。“id”变体结合了 Argon2i(抗侧信道攻击)和 Argon2d(抗 GPU 攻击)的特性,前半部分使用 Argon2i,后半部分使用 Argon2d。

scrypt 早于 Argon2id,并且同样基于内存密集型原理。bcrypt 更早,也更简单,但它有一个严格的 72 字节输入限制,这有时会导致使用长密码的应用程序出现问题——如果需要更长的输入,可以使用 SHA-256 进行预哈希处理,然后再进行 base64 编码。PBKDF2 速度较慢,但符合 FIPS 标准;OWASP 已将其 SHA-256 的迭代次数从 31 万次提高到 60 万次(2023 年)。

以下算法不应列入此列表:普通的 SHA-256、普通的 SHA-512、MD5、SHA-1 以及任何以“+ salt”结尾的自定义函数。速度是其被排除在外的关键因素。SHA-256 的设计初衷是为了在普通硬件上实现快速加密,但这与密码存储的需求恰恰相反。

实际审计中常见的加盐错误

实际的数据泄露报告不断指出同样的几个错误。

对所有用户重复使用同一个盐值会破坏整个机制——彩虹表问题再次出现,只是多了一步。使用用户名作为盐值更糟糕,因为用户名在不同系统中重复出现,而且常用用户名的彩虹表可以预先计算。盐值长度小于 16 字节时,在大规模用户群中很容易发生冲突。使用非加密随机函数(例如 `Math.random()`、`time(0)` mod something,以及该语言的默认随机数生成器)生成盐值会产生可预测的值,从而失去加盐的优势。

一个更隐蔽的错误是出于“安全”考虑将盐值存储在不同的服务器上。盐值并非秘密。将其分散存储在不同的系统上只会增加操作复杂性,而不会提升任何加密强度。应该将其与哈希值存储在同一行。现代 PHC 字符串会自动执行此操作。

2026 年审计中最大的问题在于使用普通的 SHA-256 加盐进行哈希运算并进行传输。盐可以防止彩虹表攻击,但对于 GPU 集群每秒对单个账户进行 100 亿次猜测的情况,盐却无济于事。解决方案并非添加第二个盐,而是切换到像 Argon2id 这样速度较慢的密钥派生函数 (KDF)。

最后:旧版无盐哈希。迁移是强制性的。标准做法是在下次登录时验证旧版哈希,使用新算法重新生成哈希,然后覆盖旧版哈希。对于不再登录的用户,应强制其在固定的时间段内重置密码。

安全中的盐

为什么光加盐是不够的

加盐只是一层保护,并非万无一失。它只能抵御一种特定的攻击——预先计算的加密表——但并不能揭示底层密码的强度。暴力破解仍然对弱密码有效。撞库攻击仍然对重复使用的密码有效。网络钓鱼仍然对任何密码都有效。

2026 年真正的密码安全需要四重防御。Argon2id 配合每个用户 16 字节的盐值来管理存储。胡椒值(可选,但对于敏感系统来说非常必要)可以阻止仅针对数据库的窃取。多因素身份验证可以阻止撞库攻击和大多数网络钓鱼攻击。持续的泄露监控——例如 Have I Been Pwned 及其 API 等服务——可以在用户凭据泄露的瞬间将其捕获,以便强制用户重置密码。

如果跳过任何一层安全防护,攻击者最终都会找到漏洞。仅仅依靠加盐或其他任何单一防御措施,恰恰是过去十年所有安全漏洞事后分析都认定的失效点。

任何问题?

不。用户名在不同服务中会重复出现,攻击者会预先计算常用服务的彩虹表,而可预测的加盐方法最终又会失效,导致彩虹表问题——加盐原本是为了解决这个问题。始终从 CSPRNG 中提取字节:Python 中使用 `secrets.token_bytes(16)`,Node 中使用 `crypto.randomBytes(16)`。

OWASP 2025 年指南将盐值下限设为 16 字节(128 位)。NIST 理论上允许使用 4 字节盐值,但这种大小的盐值会导致大约 6.5 万个用户在生日当天发生碰撞。而使用 16 字节盐值,碰撞概率接近 1/2^64。实际上:几乎不会发生。

是的,可以防范彩虹表查找和用户间密码重用泄露。但不能防范攻击者耐心地暴力破解特定账户。真正的安全防护需要加盐,并配合使用速度慢、内存占用高的哈希算法(例如 Argon2id),理想情况下,最好再叠加多因素身份验证 (MFA)。

盐值是在对密码进行哈希处理之前添加到密码中的随机值,它能确保即使是共享密码,每个存储的哈希值也都是唯一的。盐值并非秘密,也并非密钥,更不是加密。它与哈希值一起公开存在,并会为每个新帐户重新生成。

这是密码短语技巧,而非加盐规则。它建议用户将三个不相关的单词(例如“trumpet-glacier-velvet”)串联起来,以增加记忆性和熵值。加盐规则关乎数据库如何存储你选择的密码。而三词规则则关乎如何选择密码。两者都有助于提高安全性。

加盐操作会在密码哈希处理之前,向其中添加一个唯一的随机值。其目的是:即使两个用户使用完全相同的密码,最终也会得到两个不同的哈希值。加盐值是针对每个账户的,公开可见,并且与哈希值一起存储在数据库中。它的真正作用是消除彩虹表。

Ready to Get Started?

Create an account and start accepting payments – no contracts or KYC required. Or, contact us to design a custom package for your business.

Make first step

Always know what you pay

Integrated per-transaction pricing with no hidden fees

Start your integration

Set up Plisio swiftly in just 10 minutes.