密码哈希的盐与彩虹表 (Salt & Rainbow Table)
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/gra9a8NwdJtubcrypt 的 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. 安全边际自检清单
- 是否明文存密码?→ 必须 hash。
- 是否带盐且每用户随机?→ 必须,否则扛不住彩虹表。
- 是否慢哈希?→ bcrypt(cost≥10) / argon2id,否则扛不住穷举。
- 拖库场景下 hash 能否被秒查?→ 用上面的对比表自检。
- 多个消费者共用同一口令文件时,格式是否取交集?→ 避免单点 401。
- 传输层是否 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