密码与加密基础篇(4):bcrypt / Argon2id 为什么比 MD5 更适合存密码? 前面几篇我们已经把几个基础问题讲清楚了密码与加密基础篇1别再说 MD5 加密了编码、摘要、加密到底有什么区别-CSDN博客密码与加密基础篇2密码到底怎么存为什么 MD5 已经过时-CSDN博客密码与加密基础篇3salt 和 pepper 到底是什么为什么 salt 可以放数据库-CSDN博客第1篇MD5 不是加密而是摘要 / 哈希 第2篇密码不能明文存也不应该简单 MD5 存 第3篇salt 是公开随机扰动pepper 是服务端私有秘密这一篇继续往下讲一个更核心的问题为什么 bcrypt / Argon2id 比 MD5 更适合存密码很多老项目里密码存储可能还是这样String passwordHash md5(rawPassword);或者稍微增强一点String passwordHash md5(rawPassword salt);这套方案能跑也确实比明文存密码好。但从现代密码安全角度看它已经不够了。不是因为 MD5 不能生成 hash而是因为MD5 太快了而密码哈希恰恰不能追求快。这句话是理解 bcrypt / Argon2id 的关键。一、先说结论密码哈希不是越快越好我们平时做程序经常追求快接口要快 查询要快 缓存要快 算法要快 启动要快但是密码存储这个场景很特殊。密码哈希不能一味追求快。因为密码哈希面对的不是普通业务计算而是攻击者的离线破解。如果数据库泄漏攻击者拿到username salt password_hash他就可以在自己的机器上疯狂猜密码hash(123456 salt) hash(111111 salt) hash(password salt) hash(qwerty salt)然后和数据库里的password_hash比较。这个过程不需要再请求你的服务器。所以服务端的登录限流、验证码、风控对这种离线破解帮助有限。这时真正影响安全性的是攻击者每猜一次密码的成本。MD5 的问题就在这里它太快了。而 bcrypt / Argon2id 的设计目标就是让每次猜测都更贵。二、MD5 的问题不是“不能 hash”而是“太适合被暴力猜”MD5 当然可以把密码变成一个摘要值。比如123456 ↓ MD5 e10adc3949ba59abbe56e057f20f883e它也不可逆。也就是说不能直接从e10adc3949ba59abbe56e057f20f883e解密回123456但是攻击者不需要解密。攻击者只需要猜。比如他提前准备常见密码表123456 111111 12345678 password qwerty admin123然后逐个计算 MD5。如果算出来的结果和数据库里一样就说明猜中了。所以很多人说“MD5 被破解了”严格来说不一定是被解密而是被猜中了。密码越弱越容易被猜中。MD5 越快攻击者猜起来越便宜。这才是核心问题。三、加 salt 以后MD5 还不够吗加 salt 比不加 salt 好。比如不用 salt用户AMD5(123456) xxx 用户BMD5(123456) xxx两个用户密码一样hash 也一样。攻击者还能用通用彩虹表快速匹配。加了 salt 后用户AMD5(123456 saltA) hashA 用户BMD5(123456 saltB) hashB即使两个人密码一样最终 hash 也不同。这很好。但是还不够。因为数据库泄漏时salt 通常也在数据库里。攻击者拿到salt abc001 password_hash xxxxxx他仍然可以针对这个 salt 猜MD5(123456 abc001) MD5(111111 abc001) MD5(password abc001)salt 的作用是让攻击者不能用一张通用表横扫所有用户。但 salt 不能解决 MD5 太快的问题。所以MD5 salt 比 MD5 好但仍然不是现代密码存储的推荐方案。四、密码哈希到底需要什么能力密码哈希和普通文件摘要不一样。普通文件摘要追求的是计算快 结果稳定 方便校验比如MD5(file) SHA-256(file)用来判断文件有没有损坏、有没有被修改。但密码哈希追求的是不可逆 每个用户独立 salt 计算成本可调 抗暴力猜测 抗批量破解所以密码哈希需要“慢”。准确说不是无脑慢而是对正常用户来说可以接受。 对攻击者批量猜测来说成本很高。正常用户登录一次密码校验多花几十毫秒、几百毫秒通常可以接受。但攻击者如果要猜几千万、几亿次这个成本就会被无限放大。这就是 bcrypt / Argon2id 的价值。五、什么是慢哈希慢哈希就是专门为密码存储设计的哈希方案。它和 MD5 / SHA-256 这类快速哈希的区别是MD5 / SHA 适合快速摘要、文件校验、完整性校验。 bcrypt / Argon2id / PBKDF2 适合密码存储故意提高计算成本。也就是说慢哈希不是算法写得差而是它故意让计算变贵。它的目标不是服务普通摘要场景而是对抗密码破解场景。一句话密码哈希慢不是缺点而是安全特性。六、bcrypt 是什么bcrypt 是一种成熟的密码哈希算法。它不是加密不能解密。它适合存密码因为它有几个关键特点1. 不可逆 2. 自带 salt 3. cost 成本可调 4. 计算速度故意变慢 5. 生态成熟工程落地简单bcrypt 生成的结果一般长这样$2a$10$N9qo8uLOickgx2ZMRZoMyeIjZAgcfl7p92ldGxad68LJZdL17lhWy这串内容不是单纯的 hash。它里面通常包含算法版本 cost 参数 salt 最终 hash 结果所以使用 bcrypt 时一般不需要自己额外保存 salt 字段。框架会自动处理。七、bcrypt 的 cost 是什么bcrypt 里有一个很重要的参数cost可以理解为计算成本。cost 越高计算越慢。比如cost 10 cost 12 cost 14数值越高密码哈希和验证所需时间越长。这对正常用户来说影响的是一次登录校验。但对攻击者来说影响的是每一次猜测。如果攻击者要猜一千万次那么每次成本增加一点总成本就会非常夸张。这就是 cost 的意义让系统可以随着硬件性能提升逐步提高密码破解成本。八、bcrypt 在 Spring Security 里怎么用Spring Security 里常见写法是Bean public PasswordEncoder passwordEncoder() { return new BCryptPasswordEncoder(); }注册时public void register(RegisterRequest request) { String rawPassword request.getPassword(); String encodedPassword passwordEncoder.encode(rawPassword); User user new User(); user.setUsername(request.getUsername()); user.setPassword(encodedPassword); userMapper.insert(user); }登录时public boolean login(LoginRequest request) { User user userMapper.findByUsername(request.getUsername()); if (user null) { return false; } return passwordEncoder.matches( request.getPassword(), user.getPassword() ); }重点是passwordEncoder.matches(rawPassword, encodedPassword)这里不是解密。bcrypt 不能解密。matches()的本质是从 encodedPassword 里解析出算法信息、cost、salt ↓ 用用户输入的 rawPassword 重新计算 ↓ 比较结果是否一致所以密码验证不需要还原明文。九、Argon2id 是什么Argon2 是新一代密码哈希算法家族。其中 Argon2id 是更推荐用于密码存储的变体之一。你可以先这样理解bcrypt 成熟、稳定、工程生态好。 Argon2id 更新、更强除了 CPU 成本还强调内存成本。MD5 快主要问题是攻击者可以用 GPU、专用硬件进行大规模并行计算。bcrypt 通过 cost 增加计算成本。Argon2id 更进一步它不仅关注计算时间还关注内存消耗。也就是说攻击者不是只要拼算力还要拼内存。这会让大规模并行破解变得更贵。十、Argon2id 为什么强调内存成本攻击者破解密码时常见思路是批量并行。比如同时猜很多密码。如果一个算法只消耗 CPU而且非常快那么攻击者可以通过大量硬件并行计算。Argon2id 引入内存成本后每次计算都需要占用一定内存。这样攻击者想大规模并行就不只是 CPU 问题还会遇到内存成本问题。所以 Argon2id 的目标是提高时间成本 提高内存成本 提高并行破解成本这就是它比传统快速哈希更适合密码存储的原因。十一、bcrypt 和 Argon2id 怎么选工程里不要陷入绝对化。可以这样看1. 普通 Spring Boot / Spring Security 项目优先用 bcrypt。原因很简单生态成熟 团队容易理解 Spring Security 支持好 落地成本低 线上经验多对大多数普通业务系统来说bcrypt 已经比 MD5 好很多。2. 新系统、安全要求更高可以考虑 Argon2id。比如金融 医疗 政企 高价值账户体系 安全要求较高的新系统Argon2id 的安全设计更新也更强调抗硬件并行破解。3. 兼容性、合规、老系统场景PBKDF2 也可以考虑。PBKDF2 的优势是标准化时间久 兼容性强 一些平台和合规场景支持好所以不是只有一个正确答案。更重要的是不要再用 MD5 当密码存储方案。十二、bcrypt / Argon2id / PBKDF2 对比可以用这张表理解算法是否适合密码存储主要特点工程建议MD5不推荐太快容易被暴力猜测不要用于新系统密码存储SHA-256不推荐直接用也是快速哈希不要直接 hash 密码MD5 salt不推荐比裸 MD5 好但仍然太快老系统过渡方案PBKDF2可用多轮迭代兼容性好合规、兼容场景可选bcrypt推荐自带 saltcost 可调成熟普通项目优先选择Argon2id推荐时间成本 内存成本高安全新系统优先考虑记住MD5 的问题不是不能哈希而是太快bcrypt / Argon2id 的价值是让攻击者猜密码的成本变高。十三、密码哈希参数怎么定很多人会问bcrypt cost 设置多少合适Argon2id 内存参数设置多少合适这个没有一个脱离业务的固定答案。因为它和服务器性能、并发量、登录接口压力、安全等级都有关系。原则是在用户体验和服务器承载能力可接受的范围内让密码校验尽量贵。不要盲目设置过高。如果 cost 太高可能会导致登录接口变慢 服务器 CPU 压力变大 高并发登录时被打爆 攻击者没打进来自己先扛不住所以实际项目里应该本地压测 测试环境压测 结合登录 QPS 结合服务器资源 选择一个可接受的成本参数安全不是只看算法名也要看参数。同样是 bcryptcost 太低安全性不足。 cost 太高系统扛不住。需要平衡。十四、为什么不能用 AES 加密密码存数据库有些人会说那我不用 MD5我用 AES 把密码加密存数据库不行吗不推荐。因为 AES 是可逆加密。如果后端能解密出用户原始密码就说明系统存在一把可以还原所有密码的密钥。一旦这个密钥泄漏所有用户密码都可能被还原。而密码存储的原则是系统不应该有能力还原用户原始密码。所以密码应该用不可逆的密码哈希算法而不是可逆加密。正确方向是密码 bcrypt / Argon2id / PBKDF2 Token AES-GCM Android Keystore因为密码不需要还原。Token 后续还要拿出来请求接口所以 Token 需要可逆加密。这两个不要混。十五、客户端要不要 bcrypt 后再传给后端一般不需要。App 登录时通常是用户输入原始密码 ↓ HTTPS/TLS 加密传输 ↓ 后端收到原始密码 ↓ 后端使用 bcrypt / Argon2id 校验如果客户端先 bcrypt 再传后端又认可这个 bcrypt 值登录那么这个 bcrypt 值本身就会变成“等价密码”。攻击者拿到这个值仍然可能直接登录。所以客户端不应该把“先 hash 一下”当成传输安全方案。传输安全靠 HTTPS。密码存储安全靠后端 bcrypt / Argon2id。客户端应该做的是不保存密码 不打印密码 不把密码放 URL 不把密钥写死到 APK 通过 HTTPS 请求登录接口十六、老项目 MD5 怎么迁移到 bcrypt如果数据库里已经是MD5(password)或者MD5(password salt)不能直接把历史数据改成 bcrypt。因为你没有用户原始密码。正确做法是兼容旧算法 登录成功后升级流程用户登录 ↓ 判断数据库密码格式 ↓ 如果是旧 MD5就用旧逻辑校验 ↓ 校验成功后拿用户本次输入的原始密码 ↓ 重新生成 bcrypt ↓ 更新数据库这样用户无感知系统逐步升级。十七、建议给密码字段加算法标识为了支持迁移数据库里的密码值最好能看出算法。比如{md5}e10adc3949ba59abbe56e057f20f883e {bcrypt}$2a$10$xxxxxxxxxxxxxxxxxxxx登录时public boolean login(String username, String rawPassword) { User user userMapper.findByUsername(username); if (user null) { return false; } String storedPassword user.getPassword(); if (storedPassword.startsWith({md5})) { String oldHash storedPassword.replace({md5}, ); if (oldHash.equalsIgnoreCase(md5(rawPassword))) { String newHash {bcrypt} passwordEncoder.encode(rawPassword); userMapper.updatePassword(user.getId(), newHash); return true; } return false; } if (storedPassword.startsWith({bcrypt})) { String bcryptHash storedPassword.replace({bcrypt}, ); return passwordEncoder.matches(rawPassword, bcryptHash); } return false; }这个思路很重要不要指望一次性把所有历史密码升级掉而是让用户登录成功时顺手升级。如果安全风险比较高也可以要求用户重置密码。十八、Spring Security 的 DelegatingPasswordEncoder 思路Spring Security 里有一种很适合多算法兼容的设计DelegatingPasswordEncoder它的思想是通过前缀标识算法 根据算法选择对应 PasswordEncoder类似{bcrypt}$2a$10$xxxx {pbkdf2}xxxx {noop}xxxx这样系统就能支持多种历史密码格式。这对老系统迁移非常有用。核心思想可以借鉴到自己的认证系统里密码字段不要只存 hash。 最好能知道这个 hash 是用什么算法生成的。十九、bcrypt / Argon2id 不是万能的虽然 bcrypt / Argon2id 比 MD5 更适合存密码但它们不是万能药。它们主要解决的是数据库泄漏后攻击者离线破解密码的成本问题。它们不能直接解决用户密码太弱 登录接口被在线爆破 密码被日志打印 客户端把密码存本地 Token 泄漏 钓鱼网站骗密码 撞库攻击所以完整认证安全还需要HTTPS 密码不打日志 登录失败限流 验证码 MFA / 二次验证 弱密码限制 改密后 Token 失效 异常登录风控 Token 安全存储安全不是单靠一个算法解决的。二十、常见误区误区 1MD5 不可逆所以很安全不对。不可逆只是基本要求。MD5 太快容易被批量猜测。误区 2MD5 salt 就够了不够。salt 解决相同密码同 hash 和预计算表问题但 MD5 仍然太快。误区 3bcrypt 是加密算法不准确。bcrypt 是密码哈希算法不是加密算法不能解密。误区 4密码应该加密存登录时解密比较不推荐。密码不应该可逆存储。应该用不可逆密码哈希。误区 5客户端先 hash 一下就安全了不对。客户端 hash 值可能变成等价密码。传输安全应该靠 HTTPS。误区 6用了 bcrypt 就不需要其他安全措施不对。bcrypt 解决的是密码存储问题不解决所有认证安全问题。二十一、最终总结bcrypt / Argon2id 为什么比 MD5 更适合存密码因为密码存储的核心不是简单生成一个摘要而是提高攻击者猜密码的成本。MD5 的问题是太快 适合文件校验 不适合密码存储 即使加 salt仍然容易被离线暴力猜测bcrypt 的优势是自带 salt cost 可调 计算成本可控 工程成熟 Spring Security 落地简单Argon2id 的优势是更新一代密码哈希方案 同时强调时间成本和内存成本 更适合高安全新系统如果压缩成一句话MD5 适合做快速摘要bcrypt / Argon2id 适合做密码哈希密码哈希不是要快而是要让攻击者猜不起。再放到整个认证体系里登录传输靠 HTTPS。 密码存储靠 bcrypt / Argon2id / PBKDF2。 salt 让每个用户的 hash 独立。 pepper 作为服务端秘密增强保护。 Token 本地存储靠 AES-GCM Android Keystore。 Token 失效靠后端 Redis / jti / tokenVersion。这套分工清楚了密码安全和 Token 安全就不会再混在一起。