← 返回题目列表

密码为什么不能明文或可逆加密保存?BCrypt 如何使用?

高频 中等 第 2 / 23 题 更新于 2026/07/25
PasswordEncoderBCrypt密码安全

简化版

密码不能明文保存、也不应可逆加密保存——因为数据库一旦泄露,攻击者就直接拿到了所有用户的真实密码(还会危及用户在其他网站复用的账号)。正确做法是用带盐(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 平滑升级算法。工程细节:密码不进日志、重置链接一次性、登录失败限流、改密审计