4. 密码存储架构现状分析:旧方式识别与新方案选择

咱们做密码改造,第一步得摸清家底。说白了,就是看看系统里那些密码到底是怎么存的。

我见过太多项目,一上来就嚷嚷着要上 argon2,结果翻代码发现还在用 MD5 甚至明文……嗯,连基本情况都没搞明白,改造方案再先进也用不上。今天咱们就聊聊怎么快速识别旧存储方式,以及该怎么选新方案。

4.1 那些「历史遗留」的密码存储方式

我个人习惯,接手一个系统先扫配置文件、数据库字段,再翻注册/登录的代码。常见的雷区有下面这些:

⚠️ 注意: 以下三种存储方式都应该被视作「高危」,发现一个就改一个。

4.1.1 明文存储

这个最离谱。我在项目中遇到过某内部管理系统的用户表,password 字段直接存的是用户输入的字符串。当时我就问开发同事「你们是不是觉得数据库永远不会被拖?」他们笑了笑……后来那个系统被扫出 SQL 注入,密码全暴露了。

  • 识别特征:数据库字段长度跟原密码差不多,通常 varchar(32) 或 varchar(64) 但内容可读。
  • 代码特征:登录逻辑里直接 SELECT * FROM users WHERE password = '$input'。
  • 后果:数据库被拖,所有账号全裸奔。

4.1.2 MD5 / SHA-1 单向散列

虽然比明文好点,但在今天已经毫无防御力。为什么?因为彩虹表 + GPU 暴力破解,MD5 算一次只需要几纳秒。我2018年帮朋友审计过一个金融 App,他们还在用 MD5(不带盐)。我花了不到2小时就解开了80%的用户密码……全是常见口令。

  • 识别特征:字段长度固定 32 位(MD5)或 40 位(SHA-1),通常是十六进制字符串。
  • 代码特征:注册时 $password = md5($input); 或 sha1($input)。
  • 加强版陷阱:有人会加个固定盐 md5($salt.$input)。但盐如果写死在代码里,还是防不住 ASIC 矿机的批量破解——盐太简单,相当于换了个固定前缀,攻击者可以提前预计算。

4.1.3 可逆加密(AES、DES 等)

这比 MD5 还危险。因为可逆意味着只要拿到密钥就能还原密码。我见过不少国产中间件、或者老旧框架,默认用 AES 加密用户密码。密钥通常就硬编码在配置里。

  • 识别特征:字段是 Base64 字符串(或二进制 blob),长度不固定;解密逻辑存在代码中。
  • 后果:密钥泄露 = 全量密码泄露。

4.2 新方案的选择:bcrypt、argon2、PBKDF2

搞清楚现状之后,我们就要换方案了。选哪个?我建议按场景来。

方案 核心原理 计算耗时(推荐) 抗 GPU/ASIC 推荐场景
argon2 内存硬 + 时间硬 + 并行度可调 ≈ 50~100ms ★★★★★ 新系统、高安全要求
bcrypt Blowfish 密码 + 可调 cost ≈ 250ms(cost=12) ★★★★ 成熟兼容、老系统升级
PBKDF2 多次迭代 + 盐 ≈ 50~100ms(迭代数10万+) ★★★ 兼容 FIPS 规范、Java/.NET 生态
📌 我的建议: 如果是从零开始,直接上 argon2id(argon2 的变体,抗侧信道攻击更好)。如果是改造现有系统,优先考虑 bcrypt,因为库的兼容性最广,而且成本参数(cost)可以逐步调高,对用户无感。

4.2.1 为什么 argon2 是首选?

2015 年密码哈希竞赛胜出者。它强制消耗「内存」资源,这让 GPU 和 ASIC 机并行破解的收益大幅下降。你想想看,GPU 核心再多,显存有限,argon2 一次就要占用几十 MB 内存,同时跑几百个线程就把显存撑爆了。

💡 避坑: 我曾经在云函数(Lambda)上用过 argon2,默认内存参数 64MB,结果每次函数冷启动耗尽了可用内存,直接超时。后来我把内存降到 16MB,迭代次数相应调高,平衡了延迟和安全。记住:生产环境一定要根据服务器配置调参,别用默认值。

4.2.2 bcrypt 依然是可靠的老兵

bcrypt 从 1999 年用到现在,经过了大风大浪。它的 cost 参数(2^cost 次迭代)是可调的,对破解方来说,cost 每增加 1,破解时间翻倍。我习惯给新系统设 cost=12,生产监控发现响应慢了再降到 10,但别低于 8。

// PHP 示例:bcrypt 哈希与校验
$hash = password_hash($password, PASSWORD_BCRYPT, ['cost' => 12]);
if (password_verify($input, $hash)) {
    // 登录成功
}

4.2.3 PBKDF2 的适用场景

PBKDF2 是 NIST 推荐的标准,很多银行、政府项目强制用 FIPS 认证库,这时候 PBKDF2 是唯一合规选项。但它只抗「时间硬」,不抗「内存硬」,GPU 可以并行跑成千上万个 PBKDF2 实例。所以如果业务没有合规约束,尽量别选它。

4.3 旧密码向新方案迁移的策略

直接要求所有用户在某个时间点重置密码,大概率会被喷。我常用的做法是「渐进式迁移」:

  1. 检测存储方式:登录时判断哈希字符串前缀(如 $2y$ 是 bcrypt,$argon2id$ 是 argon2)。
  2. 在线升级:用户登录成功时,用明文密码和新算法重新生成哈希,更新数据库字段。旧哈希不用立即删,留个标记。
  3. 兜底:如果用户长时间不登录,用一个后台任务批量标记为「需强制重置密码」,或者发邮件提醒。
⚠️ 注意: 迁移过程中,如果旧哈希是纯 MD5/SHA1,千万不要在代码里先 MD5 再 bcrypt —— 这叫「二次散列」,你并没有解决 MD5 的脆弱性。正确的做法是:拿到用户输入明文,直接做新哈希。旧哈希只在兼容旧用户时作为验证中转用,验证通过后立即替换成新哈希。

4.4 知识体系:从识别到迁移的完整链路

下面这张 SVG 图总结了本章的核心逻辑。你看一眼,就能知道密码存储改造应该怎么走。

密码存储改造:从识别到迁移 旧存储方式 明文 / MD5 / SHA-1 可逆加密 识别手段 字段长度 / 前缀 / 加密函数 代码审查 + 数据库扫描 评估风险 是否允许明文对比 预估改造成本 新方案选择 argon2 → 首选 bcrypt → 兼容迁移 PBKDF2 → 合规场景 渐进式迁移 登录时检测+升级哈希 后台批量标记待重置 确定改造范围 灰度期验证 AB测试新旧哈希 监控失败率/延迟 全量切换,下线旧算法 关键原则 不原地改旧字段 先验证再下线

4.5 实战中容易踩的坑

好啦,该说的技术点都说了。最后我列几个我在项目中踩过的坑,你对照着避一避:

  • 不要在 SQL 里做密码比对:我曾经见过一个系统,登录时把用户输入的密码用 WHERE password = ? 做字符串比较,但数据库字段里存的是 bcrypt hash。结果每次查询把整个表里所有 hash 都取出来——完全是反模式。密码校验必须放在应用层。
  • 小心「自动升级」的逻辑漏洞:如果你在登录成功时把旧 MD5 升级为 bcrypt,但旧 MD5 没有盐,那么攻击者如果拿到旧数据库,仍然可以用彩虹表还原部分密码。我建议:升级时强制要求用户设置新密码,或者加一个「临时令牌」做二次验证。
  • 不要相信「本地测试 OK」:argon2 在开发机上一调用才 30ms,但云上因为内存带宽限制可能暴涨到 200ms。我在 AWS Lambda 上调过好几次参才稳定下来。所以迁移前一定要在 生产环境的规格 下做一次压测。
  • 新旧哈希字段最好分开存:别复用同一个 password 字段,加个新字段 password_hash_v2,旧字段保留为 fallback。这样万一新算法出问题,可以快速回滚。
📎 小结: 识别旧密码存储方式不难,难的是如何平稳过渡到新方案。先把存量摸清楚,再选兼容性最好的 bcrypt 或最强的 argon2,然后用渐进式迁移慢慢消化。记住我的话:千万别上来就批量改库,先做灰度。