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 生态 |
4.2.1 为什么 argon2 是首选?
2015 年密码哈希竞赛胜出者。它强制消耗「内存」资源,这让 GPU 和 ASIC 机并行破解的收益大幅下降。你想想看,GPU 核心再多,显存有限,argon2 一次就要占用几十 MB 内存,同时跑几百个线程就把显存撑爆了。
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 旧密码向新方案迁移的策略
直接要求所有用户在某个时间点重置密码,大概率会被喷。我常用的做法是「渐进式迁移」:
- 检测存储方式:登录时判断哈希字符串前缀(如
$2y$是 bcrypt,$argon2id$是 argon2)。 - 在线升级:用户登录成功时,用明文密码和新算法重新生成哈希,更新数据库字段。旧哈希不用立即删,留个标记。
- 兜底:如果用户长时间不登录,用一个后台任务批量标记为「需强制重置密码」,或者发邮件提醒。
4.4 知识体系:从识别到迁移的完整链路
下面这张 SVG 图总结了本章的核心逻辑。你看一眼,就能知道密码存储改造应该怎么走。
4.5 实战中容易踩的坑
好啦,该说的技术点都说了。最后我列几个我在项目中踩过的坑,你对照着避一避:
- 不要在 SQL 里做密码比对:我曾经见过一个系统,登录时把用户输入的密码用
WHERE password = ?做字符串比较,但数据库字段里存的是 bcrypt hash。结果每次查询把整个表里所有 hash 都取出来——完全是反模式。密码校验必须放在应用层。 - 小心「自动升级」的逻辑漏洞:如果你在登录成功时把旧 MD5 升级为 bcrypt,但旧 MD5 没有盐,那么攻击者如果拿到旧数据库,仍然可以用彩虹表还原部分密码。我建议:升级时强制要求用户设置新密码,或者加一个「临时令牌」做二次验证。
- 不要相信「本地测试 OK」:argon2 在开发机上一调用才 30ms,但云上因为内存带宽限制可能暴涨到 200ms。我在 AWS Lambda 上调过好几次参才稳定下来。所以迁移前一定要在 生产环境的规格 下做一次压测。
- 新旧哈希字段最好分开存:别复用同一个
password字段,加个新字段password_hash_v2,旧字段保留为 fallback。这样万一新算法出问题,可以快速回滚。