前两篇我们已经讲了两个基础问题:
第一篇
密码与加密基础篇(1):别再说 MD5 加密了:编码、摘要、加密到底有什么区别?-CSDN博客
讲的是:
别再说 MD5 加密了:编码、摘要、加密到底有什么区别?
第二篇
密码与加密基础篇(2):密码到底怎么存?为什么 MD5 已经过时?-CSDN博客
讲的是:
密码到底怎么存?为什么 MD5 已经过时?
这一篇继续往下讲两个经常被混淆的概念:
salt pepper很多人在第一次听到 salt 的时候,会有一个疑问:
既然 salt 也参与密码 hash,那为什么它还能存在数据库里?
攻击者拿到 salt,不就知道怎么计算了吗?
这个疑问非常正常。
因为 salt 和 pepper 都是“额外拼进去的东西”,但它们的定位完全不一样。
一句话先说结论:
salt 是公开的随机扰动,目的是让相同密码得到不同 hash;pepper 是服务端私有秘密,目的是在数据库泄漏后增加攻击成本。
一、先回顾:密码为什么要 hash?
密码不应该明文存数据库。
错误做法:
username = wu password = 123456正确方向是:
用户密码 ↓ 密码哈希算法 ↓ password_hash ↓ 存数据库登录时也不是把数据库密码解密出来比较。
而是:
用户输入密码 ↓ 重新计算 hash ↓ 和数据库里的 password_hash 比较如果一致,说明密码正确。
所以密码存储的核心是:
不可逆 可验证 抗猜测MD5 的问题是太快。
bcrypt / Argon2id / PBKDF2 这类密码哈希算法的意义,就是让攻击者批量猜密码的成本变高。
而 salt 和 pepper,就是围绕“提高破解成本”展开的两个概念。
二、salt 是什么?
salt 翻译过来叫“盐”。
它本质上是一段随机字符串。
比如:
salt = abc001注册时,后端可以这样处理:
password = 123456 salt = abc001 password_hash = hash(password + salt)数据库保存:
username = wu salt = abc001 password_hash = xxxxxx这里的hash不应该是普通 MD5,现代系统里更推荐 bcrypt / Argon2id / PBKDF2。
为了方便理解,我们先用普通hash(password + salt)表达这个过程。
三、salt 解决什么问题?
salt 主要解决两个问题。
1. 相同密码得到相同 hash 的问题 2. 通用彩虹表 / 预计算表的问题四、没有 salt 会发生什么?
假设用户 A 和用户 B 都用了同一个密码:
123456如果没有 salt:
用户A: hash(123456) = xxx 用户B: hash(123456) = xxx两个人数据库里的 password_hash 会一样。
攻击者一看就知道:
这两个人密码一样。更麻烦的是,常见密码的 hash 结果攻击者可以提前算好。
比如:
123456 -> e10adc3949ba59abbe56e057f20f883e 111111 -> 96e79218965eb72c92a549dd5a330112 password -> 5f4dcc3b5aa765d61d8327deb882cf99 qwerty -> d8578edf8458ce06fbc5bb76a58c5ca4数据库一泄漏,攻击者直接查表。
这就是所谓的预计算攻击 / 彩虹表思路。
五、加了 salt 以后有什么变化?
加了 salt 后,即使两个人密码一样,最终 hash 也不一样。
比如:
用户A: password = 123456 salt = abc001 hash = hash(123456 + abc001) 用户B: password = 123456 salt = xyz999 hash = hash(123456 + xyz999)虽然原始密码都是:
123456但因为 salt 不一样,最终 hash 结果也不一样。
这样攻击者就不能拿一张通用表直接查所有用户。
他必须针对每个用户的 salt 单独计算。
所以 salt 的作用是:
让每个用户的密码 hash 独立。六、salt 为什么可以放数据库?
这就是很多人最容易卡住的点。
salt 可以放数据库,因为它本来就不是秘密。
它不是密钥。
它的目的不是让攻击者不知道怎么计算,而是让攻击者不能提前用一张通用表解决所有人。
假设数据库泄漏后,攻击者拿到了:
username = wu salt = abc001 password_hash = xxxxxx攻击者确实知道:
hash(password + abc001)他也确实可以尝试:
hash(123456 + abc001) hash(111111 + abc001) hash(password + abc001) hash(qwerty + abc001)但他必须针对这个用户的 salt 逐个计算。
如果每个用户的 salt 都不同,他就不能用一张预计算表横扫所有用户。
所以 salt 不是为了“保密算法”。
salt 是为了“打散结果”。
你可以这样记:
salt 可以公开,因为它不是秘密;它的价值在于唯一性和随机性,而不是保密性。
七、salt 不是万能的
salt 可以防止通用彩虹表,但它不能防止暴力猜测。
因为攻击者拿到 salt 后,仍然可以不断尝试:
hash(123456 + salt) hash(111111 + salt) hash(password + salt)只要他猜中了原始密码,计算结果就会和数据库里的 password_hash 一样。
所以 salt 只解决一部分问题。
它不能解决:
用户密码太弱 攻击者离线暴力猜测 hash 算法太快这也是为什么 MD5 + salt 仍然不够。
因为 MD5 太快。
攻击者虽然要针对每个 salt 重新算,但算得非常快。
现代密码存储更推荐:
bcrypt Argon2id PBKDF2它们不只是加 salt,还会故意提高计算成本。
八、bcrypt 里的 salt 怎么处理?
bcrypt 的好处之一是:它自带 salt。
bcrypt 生成的结果大概长这样:
$2a$10$N9qo8uLOickgx2ZMRZoMyeIjZAgcfl7p92ldGxad68LJZdL17lhWy这串里面通常包含:
算法版本 cost 成本因子 salt 最终 hash所以使用 bcrypt 时,很多情况下不需要自己单独设计 salt 字段。
后端登录验证时:
passwordEncoder.matches(rawPassword, encodedPassword)框架会从encodedPassword中解析出 salt 和 cost,然后用用户输入的密码重新计算,最后比较结果。
所以在 Spring Security 里使用 bcrypt 时,通常不需要自己手动拼:
password + salt你只需要:
passwordEncoder.encode(rawPassword) passwordEncoder.matches(rawPassword, encodedPassword)九、那 pepper 又是什么?
pepper 和 salt 有点像,都是额外参与密码 hash 的值。
但它们的核心区别是:
salt 不需要保密 pepper 必须保密salt 通常每个用户不同,并且存在数据库里。
pepper 通常是服务端的一段全局秘密,不存在数据库里。
可以这样理解:
salt:公开随机扰动 pepper:服务端私有秘密例如:
password = 123456 salt = abc001 pepper = server-secret password_hash = bcrypt(password + pepper)bcrypt 本身会处理 salt。
这里的 pepper 是额外加进去的服务端秘密。
十、salt 和 pepper 的区别
可以用表格直接区分。
| 项目 | salt | pepper |
|---|---|---|
| 是否保密 | 不需要保密 | 必须保密 |
| 是否存在数据库 | 可以存在数据库 | 不应该存在数据库 |
| 是否每个用户不同 | 通常每个用户不同 | 通常全系统一个或按版本一组 |
| 主要作用 | 防止相同密码同 hash,防预计算表 | 数据库泄漏后增加破解难度 |
| 客户端知不知道 | 不需要知道 | 绝对不应该知道 |
| 丢失后影响 | 一般数据库里有,不会单独丢 | 丢了可能导致无法验证旧密码 |
一句话:
salt 是公开的,pepper 是秘密的。
十一、pepper 放在哪里?
pepper 不能放在数据库里。
因为 pepper 的价值就在于:
数据库泄漏后,攻击者仍然拿不到 pepper。所以 pepper 不应该放:
数据库 Git 仓库 application.yml 明文 Android 客户端 前端代码 APK assets BuildConfig 普通配置文件更合理的位置是:
环境变量 Docker Secret Kubernetes Secret 配置中心的加密配置 云厂商 Secret Manager Vault KMS / HSM CI/CD 注入的密钥变量普通 Spring Boot 项目中,可以先用环境变量:
PASSWORD_PEPPER=your-super-secret-pepper配置文件中引用:
security: password: pepper: ${PASSWORD_PEPPER}代码里读取:
@Value("${security.password.pepper}") private String passwordPepper;生产环境不要把真实 pepper 明文提交到 Git。
十二、客户端要不要知道 pepper?
不需要。
更准确地说:
客户端绝对不应该知道 pepper。App 登录时还是:
用户输入原始密码 ↓ HTTPS 传给后端 ↓ 后端使用 pepper + bcrypt / Argon2id 校验客户端不参与 pepper。
不要这样做:
val passwordWithPepper = password + pepper api.login(username, passwordWithPepper)因为 pepper 一旦放进客户端,就不再是秘密。
APK 可以反编译。
客户端里的所谓 secret,本质上都很难真正保密。
所以 pepper 必须留在服务端。
十三、pepper 有什么价值?
假设数据库泄漏了。
如果没有 pepper,攻击者拿到:
salt password_hash 算法参数他可以离线猜密码。
比如:
bcrypt(123456) bcrypt(111111) bcrypt(password)如果有 pepper,而 pepper 没有泄漏,攻击者拿到数据库后还缺一个关键值:
pepper真实计算逻辑是:
bcrypt(rawPassword + pepper)攻击者不知道 pepper,就无法正确验证自己猜的密码。
所以 pepper 的价值是:
即使数据库泄漏,也增加离线破解难度。十四、pepper 不是第一优先级
虽然 pepper 有价值,但它不是所有项目一上来都必须做的第一件事。
对很多普通项目来说,优先级应该是:
1. 不存明文密码 2. 不用 MD5 存密码 3. 使用 bcrypt / Argon2id / PBKDF2 4. 登录接口走 HTTPS 5. 密码不打日志 6. 登录失败限流 7. 再考虑 pepper如果你现在还在用:
MD5(password)那第一步不是纠结 pepper,而是先迁移到 bcrypt / Argon2id。
pepper 是增强项,不是替代项。
不要用 pepper 掩盖 MD5 的问题。
十五、pepper 的风险:丢了怎么办?
pepper 是秘密。
也正因为它是秘密,所以它有一个风险:
pepper 一旦丢失,旧密码可能无法验证。因为数据库里的 password_hash 是基于这个 pepper 计算出来的。
如果后端验证时拿不到原来的 pepper,用户输入正确密码也可能验证失败。
所以 pepper 要做好:
备份 权限控制 环境隔离 密钥轮换方案 生产环境保护不要让普通开发人员随便看到生产 pepper。
不要把生产 pepper 放进本地开发配置。
十六、pepper 如何轮换?
如果系统安全要求高,pepper 可能需要轮换。
可以引入版本号:
pepper_version数据库里可以保存:
password_hash pepper_version比如:
pepper_v1 pepper_v2登录时:
1. 根据用户的 pepper_version 找对应 pepper 2. 使用对应 pepper 校验密码 3. 如果校验成功,并且当前不是最新版本 4. 用新 pepper 重新生成 password_hash 5. 更新 pepper_version流程类似老 MD5 迁移到 bcrypt。
但这属于更高级的安全设计。
普通项目可以先不做这么复杂。
十七、salt、pepper、bcrypt 放在一起怎么理解?
可以这样理解:
bcrypt / Argon2id: 真正负责密码哈希,故意让计算变慢。 salt: 让每个用户的密码 hash 独立,避免相同密码 hash 相同。 pepper: 服务端额外秘密,数据库泄漏后增加破解难度。三者关系不是互相替代,而是分工不同。
bcrypt / Argon2id 是主体。 salt 是基础配置。 pepper 是额外增强。所以不要写成:
用了 salt,就不用 bcrypt。 用了 pepper,就不用 salt。 用了 pepper,就可以继续用 MD5。正确理解是:
bcrypt / Argon2id + 自动 salt 是基本盘。 pepper 是更高安全要求下的增强手段。十八、Spring Boot 里怎么封装 pepper?
可以封装一个 PasswordService。
@Service public class PasswordService { private final PasswordEncoder passwordEncoder; private final String pepper; public PasswordService( PasswordEncoder passwordEncoder, @Value("${security.password.pepper}") String pepper ) { this.passwordEncoder = passwordEncoder; this.pepper = pepper; } public String encode(String rawPassword) { return passwordEncoder.encode(rawPassword + pepper); } public boolean matches(String rawPassword, String encodedPassword) { return passwordEncoder.matches(rawPassword + pepper, encodedPassword); } }注册时:
String encodedPassword = passwordService.encode(request.getPassword()); user.setPassword(encodedPassword);登录时:
boolean matched = passwordService.matches( request.getPassword(), user.getPassword() );注意:
pepper 不应该从客户端传。 pepper 不应该存在数据库。 pepper 不应该提交到 Git。十九、常见误区
误区 1:salt 是秘密
不对。
salt 不需要保密。
它可以存在数据库里。
salt 的价值是随机性和唯一性,不是保密性。
误区 2:salt 存数据库就没意义了
不对。
salt 的意义不是防止别人知道计算方式,而是防止通用预计算表和相同密码同 hash。
误区 3:pepper 和 salt 一样,也可以存在数据库
不对。
pepper 必须和数据库分离。
否则数据库泄漏时,pepper 也一起泄漏,价值就大幅降低。
误区 4:客户端需要知道 pepper
不对。
pepper 是服务端秘密。
客户端知道了,就失去秘密意义。
误区 5:用了 pepper,就可以继续 MD5
不建议。
pepper 不能替代现代密码哈希算法。
先用 bcrypt / Argon2id,再考虑 pepper。
误区 6:salt 可以防止暴力破解
不准确。
salt 主要防预计算表和相同密码同 hash。
真正提高暴力猜测成本,要靠 bcrypt / Argon2id / PBKDF2 这类慢哈希算法,以及登录限流、验证码、风控。
二十、最终总结
salt 和 pepper 都是密码存储里的辅助机制,但它们不是一回事。
salt:
不需要保密 可以存在数据库 通常每个用户不同 用于避免相同密码得到相同 hash 用于抵抗通用彩虹表 / 预计算表pepper:
必须保密 不应该存在数据库 通常放在环境变量 / Secret Manager / KMS / Vault 客户端不应该知道 用于数据库泄漏后的额外保护如果压缩成一句话:
salt 是公开随机扰动,pepper 是服务端私有秘密;salt 解决“相同密码和预计算表”问题,pepper 解决“数据库泄漏后仍缺一个秘密”的问题。
再放进完整密码存储体系里:
密码不能明文存。 MD5 不适合现代密码存储。 bcrypt / Argon2id / PBKDF2 是主体。 salt 是基础机制。 pepper 是增强机制。 HTTPS 保护传输。 日志脱敏防止密码泄漏。 登录限流和风控防止在线暴力破解。理解到这里,密码存储这条线就基本通了。