密码与加密基础篇(3):salt 和 pepper 到底是什么?为什么 salt 可以放数据库? 📅 2026/7/13 16:16:43 前两篇我们已经讲了两个基础问题第一篇密码与加密基础篇1别再说 MD5 加密了编码、摘要、加密到底有什么区别-CSDN博客讲的是别再说 MD5 加密了编码、摘要、加密到底有什么区别第二篇密码与加密基础篇2密码到底怎么存为什么 MD5 已经过时-CSDN博客讲的是密码到底怎么存为什么 MD5 已经过时这一篇继续往下讲两个经常被混淆的概念salt pepper很多人在第一次听到 salt 的时候会有一个疑问既然 salt 也参与密码 hash那为什么它还能存在数据库里攻击者拿到 salt不就知道怎么计算了吗这个疑问非常正常。因为 salt 和 pepper 都是“额外拼进去的东西”但它们的定位完全不一样。一句话先说结论salt 是公开的随机扰动目的是让相同密码得到不同 hashpepper 是服务端私有秘密目的是在数据库泄漏后增加攻击成本。一、先回顾密码为什么要 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 的区别可以用表格直接区分。项目saltpepper是否保密不需要保密必须保密是否存在数据库可以存在数据库不应该存在数据库是否每个用户不同通常每个用户不同通常全系统一个或按版本一组主要作用防止相同密码同 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_PEPPERyour-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。十九、常见误区误区 1salt 是秘密不对。salt 不需要保密。它可以存在数据库里。salt 的价值是随机性和唯一性不是保密性。误区 2salt 存数据库就没意义了不对。salt 的意义不是防止别人知道计算方式而是防止通用预计算表和相同密码同 hash。误区 3pepper 和 salt 一样也可以存在数据库不对。pepper 必须和数据库分离。否则数据库泄漏时pepper 也一起泄漏价值就大幅降低。误区 4客户端需要知道 pepper不对。pepper 是服务端秘密。客户端知道了就失去秘密意义。误区 5用了 pepper就可以继续 MD5不建议。pepper 不能替代现代密码哈希算法。先用 bcrypt / Argon2id再考虑 pepper。误区 6salt 可以防止暴力破解不准确。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 保护传输。 日志脱敏防止密码泄漏。 登录限流和风控防止在线暴力破解。理解到这里密码存储这条线就基本通了。