当前位置: 首页 > news >正文

密码与加密基础篇(3):salt 和 pepper 到底是什么?为什么 salt 可以放数据库?

前两篇我们已经讲了两个基础问题:

第一篇
密码与加密基础篇(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 的区别

可以用表格直接区分。

项目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_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 保护传输。 日志脱敏防止密码泄漏。 登录限流和风控防止在线暴力破解。

理解到这里,密码存储这条线就基本通了。

http://www.cnnetsun.cn/news/3346789.html

相关文章:

  • TLA2518与PIC18F26K40构建高精度数据采集系统
  • IndexTTS2配置详解:从config.yaml到模型文件的完整配置手册
  • 别只把微信 API 当聊天接口:盘点生产环境下的 4 个硬核工程玩法
  • 计算机毕业设计之jsp社区门诊管理系统的设计与实现
  • Pixhawk强制硬件配置:解锁前必须完成的底层校验流程
  • 【扩散模型】DDPM与DDIM:从概率扩散到确定性采样的演进之路
  • ChatGPT Pro价格全透视:$29/月背后藏着的7项服务差异、4类用户真实付费陷阱(内部价目表首曝)
  • codex-async-mcp MCP 服务说明文档
  • DeepSeek-R1-Distill-Qwen-1.5B的搜索策略配置:beam search与sampling参数详解
  • AI 工具开发实战(10):开发一个 AI Agent 自动化工作流——让多个 Agent 协作完成复杂任务(完结篇)
  • 16K上下文长度在DeepSeek-R1-Distill-Qwen-1.5B中的应用:处理长文本的终极方案 [特殊字符]
  • 【JavaScript】键盘事件深度解析:从基础监听、组合键到输入法处理
  • 科研基金立项结果公告采集:从目录页到项目制数据入库的 Python 爬虫实战
  • Agent 高可用设计:多实例部署下的状态同步和故障转移策略
  • keras-resnet迁移学习实战:利用预训练模型解决实际问题
  • Wand-Enhancer终极指南:免费解锁WeMod专业版的高效解决方案
  • 【更新至2025年】2000-2025年上市公司年报文本数据(pdf+txt格式)
  • DAKeyboardControl实战指南:3种方法轻松处理键盘遮挡问题
  • DIFY-09:多工具协作——搜索 + 总结
  • 父母做好自身榜样,言行潜移默化影响孩童
  • GEO优化源码二次开发:企业私有化部署功能改造实战方案
  • TVA具身智能的概念、架构与应用(8)
  • 5分钟快速入门Archipel:从安装到创建第一个XMPP管理的虚拟机
  • TVA具身智能的概念、架构与应用(10)
  • 【前端+跨域】跨域问题全面解析与解决方案指南
  • Windows Defender彻底卸载工具:释放系统性能的终极解决方案
  • 我与IT三十年:1998,蝴蝶落到表单上
  • logos-in-pure-css Apple Logo实现分析:复杂形状的CSS绘制策略
  • ArkTS 搜索旧请求覆盖新结果怎么办:中式美食输入框怎么防止慢请求回写
  • 3 种混合波束成形方案对比:传统优化 vs 深度学习 vs 深度强化学习