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

密码与加密基础篇(2):密码到底怎么存?为什么 MD5 已经过时?

上一篇我们讲了一个基础概念:

MD5 不是加密,而是摘要 / 哈希。

很多老项目里,我们经常会看到这样的代码:

String password = md5(rawPassword); user.setPassword(password);

或者稍微复杂一点:

String password = md5(rawPassword + salt); user.setPassword(password);

以前很多人会说:

密码用 MD5 加密一下再存数据库。

但严格来说,这句话有两个问题:

1. MD5 不是加密,是哈希 / 摘要。 2. MD5 已经不适合作为现代密码存储方案。

那密码到底应该怎么存?

这一篇就专门讲清楚:

1. 密码为什么不能明文存? 2. 密码为什么不需要解密? 3. MD5(password) 为什么不安全? 4. MD5(password + salt) 为什么仍然不够? 5. bcrypt / Argon2id / PBKDF2 是什么? 6. Spring Security 里应该怎么落地? 7. 老项目里的 MD5 密码应该怎么迁移?

一、密码绝对不能明文存

最错误的做法是:

username = wu password = 123456

也就是数据库里直接保存用户原始密码。

这种问题非常严重。

一旦数据库泄漏,攻击者拿到的就是所有用户的真实密码。

更严重的是,很多用户会在多个平台使用相同密码。

所以一个系统泄漏,可能会导致用户在其他平台也被撞库。

因此,后端不应该保存:

用户原始密码

后端应该保存的是:

密码哈希后的结果

也就是:

rawPassword ↓ password hash ↓ 存数据库

二、密码为什么不需要“解密”?

很多人第一次接触密码哈希时,会有一个疑问:

密码哈希之后无法还原,那用户下次登录时怎么验证?

答案是:

密码验证不需要解密。

注册时:

用户输入密码:123456 ↓ 后端做密码哈希 ↓ 数据库保存 password_hash

登录时:

用户再次输入密码:123456 ↓ 后端用同样算法重新计算 ↓ 和数据库里的 password_hash 比较

如果匹配,说明密码正确。

所以密码验证的核心不是:

数据库里的密码 → 解密成明文 → 比较

而是:

用户输入的密码 → 再算一次 hash → 比较 hash

也就是说:

密码存储的目标,就是让系统自己也无法还原用户密码。

这也是为什么正规系统一般不提供“找回原密码”,而是提供“重置密码”。

因为系统自己也不应该知道用户原密码。


三、MD5(password) 的问题

早期很多项目会这样存密码:

String encodedPassword = md5(rawPassword);

比如:

rawPassword = 123456 MD5(rawPassword) = e10adc3949ba59abbe56e057f20f883e

数据库保存:

password = e10adc3949ba59abbe56e057f20f883e

登录时:

String inputPassword = request.getPassword(); String inputMd5 = md5(inputPassword); if (inputMd5.equals(user.getPassword())) { // 登录成功 }

这套逻辑能跑。

但问题是:

MD5 太快了。

快在正常业务里是优点,但在密码存储里反而是缺点。

因为一旦数据库泄漏,攻击者可以疯狂猜密码。

比如攻击者拿到:

e10adc3949ba59abbe56e057f20f883e

他不用“解密”,他只需要提前准备常见密码表:

123456 -> e10adc3949ba59abbe56e057f20f883e 111111 -> 96e79218965eb72c92a549dd5a330112 password -> 5f4dcc3b5aa765d61d8327deb882cf99 qwerty -> d8578edf8458ce06fbc5bb76a58c5ca4

一查就知道:

e10adc3949ba59abbe56e057f20f883e = 123456

这不是 MD5 被“解密”了,而是密码太常见,被猜中了。


四、那加 salt 不就行了吗?

很多项目后来升级成:

String encodedPassword = md5(rawPassword + salt);

比如:

password = 123456 salt = abc001 hash = MD5(123456 + abc001)

这比单纯 MD5(password) 要好。

因为如果两个用户密码一样,但 salt 不一样,最终 hash 也不一样。

比如:

用户A: password = 123456 salt = abc001 hash = MD5(123456 + abc001) 用户B: password = 123456 salt = xyz999 hash = MD5(123456 + xyz999)

这样至少解决了两个问题:

1. 相同密码不会得到相同 hash。 2. 通用彩虹表不能直接套所有用户。

但是,MD5 + salt 仍然不够。

为什么?

因为 MD5 还是太快。

攻击者拿到数据库后,通常也能看到 salt。

比如:

username = wu salt = abc001 password_hash = xxxxxx

攻击者可以针对这个 salt 重新猜:

MD5(123456 + abc001) MD5(111111 + abc001) MD5(password + abc001) MD5(qwerty + abc001)

salt 不需要保密,它只是让每个用户的 hash 独立。

真正的问题是:

攻击者每猜一次的成本太低。

所以现代密码存储不能只靠 MD5 + salt。


五、密码存储真正需要什么特性?

密码存储需要的不是“快”,而是“慢”。

准确说,需要这些特性:

1. 不可逆 2. 每个用户不同 salt 3. 计算成本可调 4. 抗暴力猜测 5. 抗 GPU / 专用硬件批量破解

MD5 的问题不是不能哈希。

MD5 的问题是:

太快了。

密码存储算法应该故意慢。

正常用户登录一次,慢几十毫秒、几百毫秒,用户几乎无感。

但攻击者要猜几千万、几亿个密码时,成本就会被放大。

这就是 bcrypt、Argon2id、PBKDF2 这类算法的意义。


六、bcrypt 是什么?

bcrypt 是一种专门用于密码存储的哈希算法。

它不是加密,不能解密。

它的特点是:

1. 不可逆 2. 自带 salt 3. 有 cost 成本因子 4. 可以故意变慢 5. 适合密码存储

bcrypt 生成的结果大概长这样:

$2a$10$N9qo8uLOickgx2ZMRZoMyeIjZAgcfl7p92ldGxad68LJZdL17lhWy

这串里面包含了:

算法版本 cost 成本因子 salt 最终 hash

所以使用 bcrypt 时,通常不需要自己单独设计 salt 字段。

登录验证时,框架会从这串结果中解析出 salt 和 cost,然后用用户输入的密码重新计算并比较。


七、Argon2id 是什么?

Argon2id 是更新一代的密码哈希算法。

你可以简单理解为:

bcrypt:成熟、老牌、兼容性强 Argon2id:更新、更强,强调内存成本

Argon2id 不只是让计算变慢,还会增加内存消耗。

这对攻击者批量破解更不友好。

因为攻击者不只是要拼 CPU,还要拼内存成本。

所以在安全要求更高的新系统里,Argon2id 是很好的选择。

不过在很多 Java / Spring 项目里,bcrypt 仍然非常常见。

原因是:

1. Spring Security 支持简单 2. 生态成熟 3. 团队接受度高 4. 上手成本低 5. 线上兼容性好

所以实际项目里可以先选 bcrypt,把体系跑通。


八、PBKDF2 是什么?

PBKDF2 也是一种常见的密码派生算法。

它的思路是:

对密码做多轮迭代计算,让计算变慢。

比如:

不是 hash 一次,而是 hash 很多次。

PBKDF2 标准化时间久,兼容性强,在一些系统和合规场景里也经常见到。

所以常见推荐方案是:

Argon2id bcrypt PBKDF2

这三个都比直接 MD5(password) 更适合密码存储。


九、MD5、bcrypt、Argon2id 的区别

可以用这张表理解。

算法能不能还原是否适合密码存储主要问题 / 特点
MD5不能不推荐太快,容易被暴力猜测
SHA-256不能不推荐直接用于密码也是快速哈希
MD5 + salt不能不推荐解决相同密码同 hash,但仍然太快
bcrypt不能推荐自带 salt,cost 可调,成熟
Argon2id不能推荐更新,内存成本更强
PBKDF2不能可用多轮迭代,兼容性好

注意:

不能还原不是问题。

密码本来就不应该能还原。

真正要看的是:

攻击者猜密码的成本够不够高。

十、Spring Security 里怎么落地?

在 Spring Security 里,不建议自己手写 MD5。

更推荐使用 PasswordEncoder。

比如使用 bcrypt:

@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)

matches()不是解密。

它是用用户输入的原始密码,结合 encodedPassword 中的算法信息、salt、cost,重新计算并比较。


十一、为什么不建议客户端 MD5 后再传?

很多 App 老项目里会这样:

val passwordMd5 = md5(password) api.login(username, passwordMd5)

看起来好像安全,因为没有直接传原始密码。

但问题是:

passwordMd5 本身变成了等价密码。

也就是说,后端如果认可这个 MD5 值登录,那么攻击者只要拿到这个 MD5 值,就不需要知道原始密码,也可以直接登录。

所以客户端 MD5 不是安全方案。

正确做法通常是:

客户端输入原始密码 ↓ 通过 HTTPS/TLS 传输 ↓ 后端用 bcrypt / Argon2id / PBKDF2 校验

注意,这里的“原始密码”不是 HTTP 明文裸奔。

它必须走 HTTPS/TLS 加密通道。

客户端要做的是:

不保存密码 不打印密码 不把密码放 URL 不自己用 MD5 伪装成安全

十二、密码和 Token 的处理方式不一样

这个点非常关键。

密码和 Token 不是一类东西。

1. 密码

密码后端只需要验证,不需要还原。

所以用:

bcrypt / Argon2id / PBKDF2

也就是不可逆哈希。

流程:

用户输入密码 ↓ 密码哈希 ↓ 数据库保存 hash

2. Token

Token 后续还要拿出来请求接口。

比如:

Authorization: Bearer accessToken

所以 Token 不能用 MD5。

如果你写:

MD5(token)

那后面就拿不回原始 Token 了。

Token 本地存储应该用:

AES-GCM 加密 Android Keystore 保护 AES key

流程:

Token 明文 ↓ AES-GCM 加密 ↓ Token 密文 ↓ 本地保存

请求时:

Token 密文 ↓ AES-GCM 解密 ↓ Token 明文 ↓ Authorization Header

所以一句话:

密码用哈希,因为不需要还原;Token 用加密,因为后面还要还原出来使用。


十三、老项目已经用了 MD5,怎么办?

如果老项目数据库里已经存了:

password = MD5(password)

或者:

password = MD5(password + salt)

不要直接全部改成 bcrypt。

因为你没有用户原始密码,无法直接重新计算 bcrypt。

正确方式是:

兼容旧密码 登录成功后升级

流程:

用户登录 ↓ 发现数据库里是旧 MD5 格式 ↓ 用旧 MD5 逻辑验证 ↓ 验证成功 ↓ 拿到用户这次输入的原始密码 ↓ 重新生成 bcrypt ↓ 更新数据库

这样用户无感知,系统逐步迁移。


十四、密码算法最好带版本标识

为了支持迁移,数据库里的密码字段最好能看出算法。

比如:

{md5}e10adc3949ba59abbe56e057f20f883e {bcrypt}$2a$10$xxxxxxxxxxxxxxxx

或者单独加字段:

password_hash password_algo

我更推荐第一种风格,因为很多框架也支持类似格式。

例如:

{bcrypt}$2a$10$N9qo8uLOickgx2ZMRZoMye...

这样登录时可以根据前缀判断:

{md5} → 用旧 MD5 校验 {bcrypt} → 用 BCryptPasswordEncoder 校验

十五、MD5 到 bcrypt 的迁移示例

示例代码:

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}", ""); String inputHash = md5(rawPassword); if (oldHash.equalsIgnoreCase(inputHash)) { 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 {noop}123456 {pbkdf2}xxxx

前面的{bcrypt}{pbkdf2}就是算法标识。

登录时根据标识选择对应的 PasswordEncoder。

这个思想很适合老系统迁移。

你也可以自己实现类似逻辑。


十七、密码存储还要配合哪些安全措施?

密码哈希不是唯一安全措施。

完整登录安全还包括:

1. 登录接口必须走 HTTPS。 2. 密码不能打印日志。 3. 登录失败要限流。 4. 多次失败可以加验证码。 5. 高风险登录可以短信 / 邮箱验证。 6. 修改密码后让旧 Token 失效。 7. 数据库密码字段不能返回给前端。 8. 管理后台不能展示用户密码。 9. 生产环境不能把请求体完整打到日志里。

密码哈希解决的是:

数据库泄漏后,密码不容易被还原。

但它不解决:

接口暴力破解 日志泄漏密码 弱密码 撞库攻击 Token 泄漏

所以密码安全要放在整个认证体系里看。


十八、常见错误总结

错误 1:密码明文存数据库

严重错误。

password = 123456

错误 2:认为 MD5 是加密

不准确。

MD5 是摘要,不能解密。


错误 3:客户端 MD5 后再传就安全了

不对。

MD5 值会变成等价密码。


错误 4:MD5 + salt 就足够了

不够。

salt 解决相同密码同 hash 和彩虹表问题,但 MD5 仍然太快。


错误 5:登录时把数据库密码解密出来比较

不应该这样设计。

密码应该不可逆,登录时重新计算 hash 后比较。


错误 6:用 AES 加密密码存数据库

也不推荐。

如果用 AES,说明后端理论上可以解密出用户原始密码。

密码存储不应该可逆。

密码应该使用专门的密码哈希算法。


十九、最终建议

如果是新项目,建议直接:

bcrypt / Argon2id / PBKDF2

如果是 Spring Boot / Spring Security 项目,先用 bcrypt 就够了:

@Bean public PasswordEncoder passwordEncoder() { return new BCryptPasswordEncoder(); }

如果是老项目已经用了 MD5:

不要直接强行替换。 先兼容旧 MD5。 用户登录成功后,升级成 bcrypt。

如果安全要求更高:

可以考虑 Argon2id。 可以考虑 pepper。 可以增加登录限流、验证码、MFA、风控。

二十、总结

密码到底怎么存?

一句话:

密码不应该明文存,也不应该用 MD5 简单摘要存,而应该使用 bcrypt / Argon2id / PBKDF2 这类专门的密码哈希算法。

再展开一点:

密码不需要还原,所以不要用可逆加密。 MD5 不是加密,是摘要。 MD5 太快,不适合现代密码存储。 salt 可以让相同密码 hash 不一样,但不能解决 MD5 太快的问题。 bcrypt / Argon2id / PBKDF2 会故意提高计算成本,让攻击者批量猜密码变得更困难。 登录验证不是解密密码,而是用用户输入的密码重新计算后比较。 老项目 MD5 密码可以通过“登录成功后升级”的方式平滑迁移。

如果用一句话串起来:

密码用哈希,因为不需要还原;Token 用加密,因为后面还要使用。密码哈希要慢,Token 加密要可逆。

这句话理解了,密码存储和 Token 存储就不会再混了。

http://www.jsqmd.com/news/1058298/

相关文章:

  • 图算法(下)——MST 与最大流 — 从零精通算法与数据结构——Google 面试系统备战 第14篇
  • 2026专业的张家港办理公司变更业务企业推荐哪家强 - 品牌排行榜
  • Photon光影包:3步打造Minecraft电影级视觉体验的终极指南
  • 对称群表示理论及其在物理计算中的应用
  • 构建可信赖弹性CPS:可解释AI与运行时验证的工程实践
  • 2026秦皇岛防水补漏避坑指南:卫生间/厨房/阳台/屋顶/地下室漏水检测维修全攻略,正规施工+透明报价+口碑榜靠谱服务商推荐 - 安佳防水
  • 从混乱到高效:项目管理经典书籍推荐
  • 卡梅德生物科普IL5R(白细胞介素5受体)
  • 如何用Play Integrity API Checker快速检测Android设备安全
  • 咏巷炸鸡_小成本创业加盟_低投入品牌推荐 - 3158GEO
  • 计算几何 — 从零精通算法与数据结构——Google 面试系统备战 第15篇
  • 5大音乐平台加密文件破解:浏览器内本地解密工具深度解析
  • 2026年近期江西知名的业务外包服务商怎么联系?众诚人力资源专业解析 - 品牌鉴赏官2026
  • SQL注入深度解析:从攻击分类到实战防御策略
  • GEO代运营收费标准 四种模式拆解对比哪家更划算 - 3158GEO
  • 2026年当下,如何甄别真正具备未来竞争力的无人驾驶洗地机供应厂家? - 品牌鉴赏官2026
  • 2026降AIGC工具亲测:10款网站对比,学术合规技巧盘点
  • 3分钟解锁B站缓存宝藏:你的m4s视频转换秘籍
  • 嵌入式系统互连技术选型:以太网与RapidIO的深度对比与实战指南
  • “恒宇杯”第六届辽宁省大学生金相技能大赛暨“徕卡杯”第十五届全国大学生金相技能大赛复赛(辽宁赛区) - 品牌发掘
  • 武汉市江汉区房屋修缮|维小达|窗户维修、吊顶维修、壁纸壁布、墙面维修、石材修复、瓷砖美缝、瓷砖维修全屋一站式旧房翻新破损修护服务 - 维小达科技
  • 2026年“恒宇杯”第十五届全国大学生金相技能大赛广西区选拔赛暨广西分区赛 - 品牌发掘
  • 2026石家庄防水补漏避坑指南:卫生间/厨房/阳台/屋顶/地下室漏水检测维修全攻略,正规施工+透明报价+口碑榜靠谱服务商推荐 - 安佳防水
  • 3分钟搭建同花顺自动化交易系统:Python量化交易终极指南
  • 2026年近期,好的1-氯丙烷公司推荐:骋源高新材料实力解析 - 品牌鉴赏官2026
  • Windows系统文件ieframe.dll丢失找不到问题解决
  • FanControl终极配置指南:Windows风扇控制软件的完整解决方案
  • Switch破解终极指南:5分钟快速部署Atmosphere大气层系统与性能优化方案
  • 2026玉林漏水检测维修本地口碑防水商家榜单:厨卫/阳台/屋面/地下室渗漏水维修,持证施工+明码实价,防水补漏公司TOP5推荐 - 即刻修防水
  • 大模型微调中的幻觉问题:自蒸馏与参数冻结的解决方案