1. 一句话定位

密码哈希(Password Hashing) 是把用户密码通过单向函数变成一串定长乱码存进鉴权文件/数据库的过程;盐(Salt) 是生成时随机产生、和密码拼在一起再 hash 的一段随机串,它让"同一个密码算出的 hash 每次都不同",从而使彩虹表(Rainbow Table) 这类预先算好的查表攻击失效。两者共同决定了存储层扛不扛得住"哈希文件被拖走后的离线爆破"。

关联:这些概念落地到工具层就是 Htpasswd 基础鉴权 里 -B(bcrypt 有盐) vs -s(SHA-1 无盐) 的选择;落到鉴权架构层见 Web 登录鉴权 (Session, Cookie, Bearer, JWT)。

2. 为什么不能明文存密码

服务端若直接存明文,数据库一拖库(数据泄露)所有账号当场沦陷,且用户跨站复用密码会连累其他站点。所以存储层必须满足:即使运维人员拿到数据库,也无法还原出明文——只能"正向算、反向算不了"。

这就要求用单向哈希(One-way Hash):存的是 H(password),验密码时算 H(输入) 再比对。但单纯 hash 不够,下面一步步看为什么。

3. 无盐 vs 有盐:核心区别

盐(Salt) 是一段每次生成时随机产生的串,长度足够(如 128 bit)、每个用户/每次生成各不相同,和密码拼接后再 hash。盐明文保存(嵌在 hash 字符串里或单独一列),不需要保密,只要满足"随机且全局唯一"。

3.1 决定性的对比:同一个密码算出的 hash

用 Complicate0705{ 这个密码实地演示(本机 openssl / htpasswd):

# 无盐 {SHA} —— 生成 3 次,完全相同(全世界唯一)
HsZTIaTt12YltlOr9dJh9bUKrrQ=
HsZTIaTt12YltlOr9dJh9bUKrrQ=
HsZTIaTt12YltlOr9dJh9bUKrrQ=

# 有盐 bcrypt —— 同一密码生成 3 次,每次不同(随机盐不同)
$2y$05$q4byWb/lm4HjiSsYsiBWEejkFOUrn8luZ0DNLa98jPYDDLXUDCtXi
$2y$05$LdEh5HLY6M8yR3x2mX5RA.dkTXpehn1PDDM5JVmgMO1v9uaOjb1kS
$2y$05$kxKDoCVpLyoAqL9lxW6cCOicn5d1OEZWT0LEPzf4/gra9a8NwdJtu

bcrypt 的 hash 结构自解释,盐就嵌在里面明文存着:

$2y$05$iISckglMzlWq05HqahfpU.r1HYQZTrg58rrblSCyhQXrGhNdugUS6
 版本 cost      128位随机盐         hash 输出部分

验密码时:从存储的 hash 里取出盐 → 把用户输入的密码和这个盐拼接 → 重新算一遍 → 比对 hash 输出部分。盐不需要保密,它的作用不是"藏",而是"让每个 hash 各不相同"。

3.2 加盐带来的三个实际收益

收益无盐有盐
防彩虹表sha1("password") 全世界定值,攻击者预算几十亿常见密码的表,拖库后秒查每个用户盐不同,预计算的表作废,必须逐盐爆破
防同密码暴露两人用同一密码 → 数据库里两行 hash 一模一样,一眼看出盐不同 → hash 不同,看不出谁和谁用同密码
防跨库复用一个无盐库泄露,其 hash 可直接拿去别的无盐库比对盐不同则无法跨库复用爆破成果

关键认知:盐不是密钥。 盐明文存、可以公开;它的价值纯粹来自"随机 + 每用户唯一",而非保密性。这和密钥(必须保密)是两回事。

4. 彩虹表攻击(Rainbow Table)

彩虹表 是一种以空间换时间的预计算攻击手段:攻击者提前把海量候选密码(常见密码字典、变种、乃至一定长度内全部组合)的 hash 全算好,存成一张"hash → 明文"的反查表。拖库拿到 hash 后,不再需要现场爆破,直接查表秒得明文。

4.1 为什么无盐扛不住

无盐时 H(password) 是确定性函数:同一个密码在任何机器、任何库上算出的 hash 永远相同。攻击者可以一次性预计算一张通用表,之后攻击所有用同一算法的无盐库都适用——预计算成本被无穷多个受害者摊薄,边际成本趋零。

彩虹表相比纯哈希链(hash chain)用巧妙的存储结构,用远小于"存全部明文-哈希对"的空间换取接近 O(1) 的查询,这正是它得名"彩虹"的由来(不同归约函数像彩虹的不同色带)。

4.2 为什么有盐就让彩虹表失效

盐一加,H(password + salt) 中 salt 每用户不同,意味着:

  • 攻击者无法再预计算一张"通用表"——每个用户的 hash 都需要针对该盐单独算一张表。
  • 若库里有 N 个用户,攻击成本 × N,且每个用户的预计算表只对该用户有效,无法复用到其他用户。
  • 当 N 大、盐随机时,预计算的成本/空间超过直接在线爆破,彩虹表彻底失去意义。

这就是加盐的本质:把"一次性预算攻击所有库"退化成"对每个盐单独攻击",把规模经济打掉。

4.3 彩虹表 ≠ 暴力破解

维度彩虹表(预算攻击)暴力破解(在线/离线穷举)
时机拿到 hash 之前就预计算好拿到 hash 之后逐个试
资源存储为主(空间换时间)算力为主(时间换结果)
克星盐(让预计算作废)慢哈希/cost(让单次算很慢)
对 GPU 友好度算 SHA-1/MD5 极快,表可做得巨大bcrypt/Argon2 故意慢且吃内存,GPU 效率低

理解这点就不会把"加盐"和"调 cost"混为一谈(见下节)。

5. 慢哈希与 cost:扛暴力破解

盐防的是"查表",不防"拿到 hash 后逐个密码慢慢试"。后者要靠慢哈希(slow hash function)——故意把单次计算设计得很慢、很耗内存,让攻击者每秒能试的次数大幅下降。

5.1 cost / work factor

bcrypt 的 05($2y$05$)是 cost,即 work factor,控制内部循环轮数(2^cost 次)。越大越慢、越抗 GPU 暴力,但登录时校验也越慢,需权衡:

cost 5  : 单 GPU 每秒可试 ~数千次(演示/本机调试用)
cost 10 : 每秒几十~几百次(常规推荐下限)
cost 12+: 每秒个位数~几十次(高敏感场景)

argon2 / scrypt 在此之上还故意占大量内存,让 GPU/ASIC 的大规模并行变贵(内存硬,memory-hard),是更现代的选择。

5.2 盐 vs cost,别混淆

防御对象机制一句话
彩虹表 / 预计算盐让"同一密码"算出不同 hash,预计算的通用表作废
在线/离线穷举慢哈希 + cost让每次猜测都贵,2^cost 轮放大单次成本

只加盐但用 SHA-1(快哈希)→ 防了彩虹表,但 GPU 仍能每秒算几亿次逐个试,弱密码仍秒破。只调 cost 但无盐 → 扛了单次穷举,但一张预算表仍通吃全库。两者职责不同,都要有。bcrypt/argon2 默认就同时提供"内置随机盐 + 可调 cost",所以现代场景直接用它们即可。

6. 主流算法横向对比

算法前缀/形态盐慢哈希抗 GPU结论
明文 / {PLAIN}明文无否—❌ 禁止
SHA-1({SHA}){SHA}base64(sha1)无否极差❌ 拖库即被查表
SSHA({SSHA}){SSHA}base64(sha1+salt)有否差⚠️ 防了彩虹表但不防穷举
Apache MD5($apr1$)$apr1$salt$hash有弱一般⚠️ 旧默认,偏弱
bcrypt($2y$/$2b$)$2y$cost$salt+hash有(内嵌)是好✅ 通用首选
argon2id$argon2id$...有(内嵌)是 + 内存硬优✅ 现代首选(需新依赖)

选型一句话:新系统用 argon2id;存量/兼容场景 bcrypt(cost≥10) 是稳妥下限;{SHA} 仅在"两侧实现都只能认它"的兼容场景临时用,且必须接受其弱安全性。

7. 一个常见的兼容性坑(本机实例)

当 nginx 原生 auth_basic 和自研 Node 鉴权服务共用同一个 htpasswd 文件时,hash 格式必须取两者支持的交集,否则会出现:"Basic 弹窗登录正常,但表单 /api/login 对任何密码都 401"的怪象——因为 nginx 原生支持 bcrypt,而 Node 端只认 {SHA}/{SSHA}/{PLAIN},不认 $2y$。

详见 htpasswd bcrypt 与 auth-api 格式不兼容导致登录全 401,这是一次真实的排错记录。

8. 安全边际自检清单

  1. 是否明文存密码?→ 必须 hash。
  2. 是否带盐且每用户随机?→ 必须,否则扛不住彩虹表。
  3. 是否慢哈希?→ bcrypt(cost≥10) / argon2id,否则扛不住穷举。
  4. 拖库场景下 hash 能否被秒查?→ 用上面的对比表自检。
  5. 多个消费者共用同一口令文件时,格式是否取交集?→ 避免单点 401。
  6. 传输层是否 HTTPS?→ hash 再好,明文 HTTP 传输等于裸奔。

9. 适用边界

  • 本文讨论的是存储层防护(密码落到磁盘后怎么扛拖库)。
  • 传输层(HTTPS)、应用层(限流、锁定、二次因子)、会话层(登出/过期/Token 轮换)是不同层面,见 Web 登录鉴权 (Session, Cookie, Bearer, JWT)。
  • htpasswd 是这套概念在"管理后台/内网面板"场景的最小落地工具,命令与文件格式见 Htpasswd 基础鉴权。

一句话总结: 加盐让"同一密码"算出不同 hash,使预计算的彩虹表失效;慢哈希(cost)让每次穷举都贵,使暴力破解不可行。两者职责不同但互补,现代系统直接用 bcrypt/argon2 即可同时满足,无盐快哈希(SHA-1)是历史遗留,不该再用。


本文归档于个人知识库 · 领域:CS & AI · 模块:04 Project_CS_AI/Web