密码为什么不能明文或可逆加密保存?BCrypt 如何使用?
简化版
密码不能明文保存、也不应可逆加密保存——因为数据库一旦泄露,攻击者就直接拿到了所有用户的真实密码(还会危及用户在其他网站复用的账号)。正确做法是用带盐(salt)的慢哈希算法(如 BCrypt)——哈希是单向的,存的是不可逆的哈希值,登录时用 PasswordEncoder.matches(原密码, 哈希) 校验是否一致,不需要解密。Spring Security 用 PasswordEncoder 抽象,推荐存带算法前缀的哈希({bcrypt}...),配合 DelegatingPasswordEncoder 平滑升级算法。别用 MD5/SHA 直接存密码(太快、易爆破)。
详细版
为什么不能明文/可逆加密:
| 存储方式 | 泄露后果 |
|---|---|
| 明文 | 直接暴露所有密码 |
| 可逆加密 | 拿到密钥即可解出所有密码 |
| 普通快哈希(MD5/SHA) | 太快,GPU + 字典高速爆破 |
| 带盐慢哈希(BCrypt) | ✅ 每次猜测成本高,且盐防彩虹表 |
Spring Security 用法:
@Bean
PasswordEncoder passwordEncoder() {
// 委派编码器:存储带前缀,如 {bcrypt}$2a$10$...
return PasswordEncoderFactories.createDelegatingPasswordEncoder();
}
// 注册/改密:encode
String hashed = passwordEncoder.encode(rawPassword); // 存这个
// 登录:matches(不是先 encode 再字符串比较!)
boolean ok = passwordEncoder.matches(rawInput, hashed);
BCrypt 哈希串结构:
$2a$10$N9qo8uLOickgx2ZMRZoMy...
│ │ └─ salt + hash(盐藏在里面)
│ └─ cost 因子 = 10(2^10 次迭代)
└─ 算法版本
完整版教学
一、密码存储的威胁模型
设计密码存储,要先想清楚真正要防什么——防的是「数据库泄露后的离线爆破」。
场景:攻击者通过 SQL 注入、内部泄露、备份丢失等拿到了用户表。此时:
- 如果存的是明文 → 所有密码直接暴露,灾难。
- 如果存的是可逆加密(AES 等)→ 攻击者只要再拿到密钥(密钥往往和数据一起泄露),就能解出所有密码。
- 如果存的是哈希 → 攻击者拿到的是哈希值,不能直接还原密码,只能在自己机器上离线尝试(猜一个密码、算哈希、比对)。
密码泄露还有连带危害:很多用户在多个网站复用密码,一个站泄露可能危及用户的邮箱、银行等账号。所以密码存储是安全的底线。
| 存储方式 | 攻击者拿到用户表后的情况 | 是否推荐 |
|---|---|---|
| 明文 | 直接获得所有真实密码 | 绝不 |
| 可逆加密 | 拿到密钥即可批量解密 | 不用于密码 |
| MD5/SHA 快哈希 | 可用 GPU + 字典高速爆破 | 不推荐 |
| BCrypt/Argon2 慢哈希 | 每次猜测成本高,盐防彩虹表 | 推荐 |
密码存储的目标不是“绝对无法被猜中”,而是让数据库泄露后的离线爆破成本高到不可接受。
二、哈希和加密的区别(核心)
- 加密(Encryption)强调「可逆」:用密钥加密,用密钥能解密还原。适合「需要拿回原文」的场景(如加密传输、加密存储敏感文档)。
- 哈希(Hash)是「单向」:只能算出哈希值,无法从哈希还原原文。
密码存储要用哈希,不是加密——因为验证密码根本不需要拿回原密码,只需要判断「用户这次输入的密码,哈希后是否等于存的哈希」。既然不需要还原,就用不可逆的哈希,从根上杜绝「解密泄露」。
推论:「忘记密码」不应该能「找回原密码」(能找回说明存了可逆的,不安全),而应该走重置流程(发重置链接、设新密码)。
三、为什么不能用普通摘要(MD5/SHA)
MD5、SHA-1、SHA-256 虽然也是单向哈希,但它们是为「快速」设计的(用于完整性校验、签名),恰恰不适合密码存储:
- 太快 → 攻击者能用 GPU 每秒尝试数十亿次,配合字典(常见密码表)和彩虹表,快速爆破弱密码。
- 无盐 → 相同密码的哈希相同,可用预计算的彩虹表直接查。
密码哈希算法要故意「慢」且「带盐」——慢是为了提高每次猜测的成本(爆破一个密码要更久),盐是为了让相同密码哈希不同(防彩虹表、防批量破解)。BCrypt、Argon2、scrypt、PBKDF2 都是专为密码设计的慢哈希。
cost = 10 -> 约 2^10 轮
cost = 12 -> 约 2^12 轮,是 cost=10 的 4 倍工作量
四、BCrypt 的工作方式
BCrypt 是密码哈希的经典选择,特点:
- 自动带随机盐:每次
encode都生成随机 salt,所以同一个密码两次哈希结果不同(盐不同)。盐直接存在哈希串里(不用单独存字段)。 - cost 因子(工作因子):
$2a$10$...里的10表示迭代 2^10 次。cost 越高,计算越慢、爆破成本越高。随着硬件变强,可以调高 cost 保持安全(如从 10 调到 12)。 - 校验时:
matches会从存储的哈希串里读出盐和 cost,对用户输入执行同样的计算,再比较结果。
所以 BCrypt 一个哈希串就自包含了「算法版本 + cost + 盐 + 哈希」,验证时全能取到。
五、Spring Security 的正确用法
Spring Security 用 PasswordEncoder 抽象密码处理:
- 注册/改密:
passwordEncoder.encode(rawPassword)→ 存储结果。 - 登录:
passwordEncoder.matches(rawInput, storedHash)→ 返回是否匹配。
关键坑:不要「先 encode 再字符串比较」:
// ❌ 错误:随机盐导致每次 encode 结果不同,永远不相等
if (encoder.encode(input).equals(storedHash)) { ... }
// ✅ 正确:用 matches,它会用存储哈希里的盐重新计算
if (encoder.matches(input, storedHash)) { ... }
因为 BCrypt 每次 encode 用新的随机盐,encode(input) 和 storedHash 的盐不同,结果自然不等。必须用 matches——它从 storedHash 里取盐来算。
六、DelegatingPasswordEncoder 的价值
Spring Security 默认的 DelegatingPasswordEncoder 让存储的哈希带算法前缀:{bcrypt}$2a$10$...、{argon2}...。这个前缀标明「这个密码用什么算法哈希的」,价值在于算法平滑升级:
- 系统里可能有历史用不同算法存的密码,前缀让系统知道每个该用哪种算法校验。
- 未来要迁移到更强的算法(Argon2)或提高 cost 时,可以在用户登录成功后(此时拿到了明文)用新算法重新哈希(rehash)并更新存储——逐步迁移,不用强制所有用户改密。
这是密码算法演进的标准做法,{prefix} 是它的基础。
七、工程安全细节
密码安全不只是选对哈希算法,还有一堆工程细节:
- 密码输入不写日志(日志脱敏,绝不打印原密码)。
- 重置链接短期有效 + 一次性使用(用完失效,防被拦截重用)。
- 登录失败要限流(防在线暴力破解——离线爆破靠慢哈希防,在线爆破靠限流/锁定防)。
- 管理后台改密要审计(谁改了谁的密码要有记录)。
- pepper(服务端密钥):可作为额外的服务端秘密(哈希时混入),增加一层防护,但不能替代盐和哈希,且要考虑密钥轮换。
八、常见误区与追问
- 误区:密码可逆加密保存更安全。 可逆意味着拿到密钥就能还原所有密码,而密码验证根本不需要还原原文。
- 误区:MD5/SHA 是哈希所以适合存密码。 它们太快,适合完整性校验,不适合抵抗离线爆破。
- 误区:BCrypt 同一个密码两次结果不同说明不能校验。 结果不同是因为随机盐,
matches会从存储哈希中取盐和 cost 重新计算。 - 追问:为什么不能先
encode再字符串比较? 每次encode会生成新盐,结果自然不同,必须用PasswordEncoder.matches。 - 追问:DelegatingPasswordEncoder 的前缀有什么价值?
{bcrypt}等前缀让系统知道用哪个算法校验,并支持以后平滑升级算法。 - 追问:pepper 能替代 salt 吗? 不能。pepper 是服务端额外秘密,salt 是每个密码的随机值,二者作用不同。
九、加强记忆
密码不能明文/可逆加密存——数据库泄露就等于密码全暴露(还危及用户在别处复用的账号)。要用带盐的慢哈希(BCrypt):哈希单向不可逆,验证只需判断输入哈希后是否等于存储值,不需解密(所以「忘记密码」走重置而非找回)。别用 MD5/SHA(太快、无盐,GPU+字典/彩虹表可爆破)——密码哈希要故意慢(提高猜测成本)+ 带盐(防彩虹表)。BCrypt 自动带随机盐(同密码两次哈希不同)、有 cost 因子(可随硬件调高)。Spring Security 用 PasswordEncoder:注册用 encode、登录用 matches(不能先 encode 再字符串比较,随机盐会导致永不相等)。DelegatingPasswordEncoder 存带前缀哈希({bcrypt}...),支持登录时 rehash 平滑升级算法。工程细节:密码不进日志、重置链接一次性、登录失败限流、改密审计。