密码存储最佳实践:加盐与慢哈希算法归纳总结

密码存储这事儿,我看着太多团队栽过跟头。你想想看,用户把密码交给你,你总不能明文存吧?就算哈希了,也不代表安全。我见过不少项目,直接用MD5或SHA-1算一遍就入库,结果被拖库后彩虹表一查,明码直接出来。说白了,哈希不是万能的,你得加盐,还得用慢哈希。

为什么不能直接哈希?

普通哈希(MD5、SHA-256)算得太快了。GPU一开,每秒几十亿次。配合预计算的彩虹表,市面上常见的弱口令几乎秒破。为什么会这样?因为相同密码产生的哈希值完全一样,攻击者把所有常见密码的哈希算好存起来,查表就行。

我在项目中遇到过客户问:“我用了SHA-256,怎么还被破解了?”我问他盐呢?他说没加盐。结果攻击者用公共彩虹表,几小时就把大部分用户密码还原了。那次之后,我要求所有项目必须加盐,而且必须用慢哈希。

加盐:第一道防线

盐是一段随机字符串,每个用户每个密码都不同。你存的时候把盐和哈希结果一起存;验证时把用户输入的密码加上同一个盐,再哈希,比较结果。这样一来,相同密码产生的哈希不同,彩虹表彻底失效。

加盐黄金法则
  • 盐必须随机,长度至少16字节(128位),推荐用密码学安全的随机数生成器。
  • 盐必须每个密码唯一,不能全局共享一个盐。
  • 盐可以明文存储,没什么可保密的。

慢哈希:第二道防线

加盐只是第一步,如果哈希还是快哈希(SHA-256、MD5),攻击者可以针对单个用户暴力枚举。慢哈希算法通过大量迭代、内存消耗或并行限制,让每次哈希计算变慢。按毫秒级算,攻击者想穷举一个8位密码,时间成本会从几天变成几十年。

目前主流的慢哈希有三种,我选了一个对比表,你一看就明白。

算法 设计特点 推荐场景 我的踩坑提醒
bcrypt 基于Blowfish;迭代次数可调 通用Web应用(最常用) 成本因子(cost)至少设10;我一般用12
scrypt CPU + 内存消耗双约束 对GPU/ASIC攻击有更高抵抗需求 参数配置复杂,容易调错
Argon2 内存、CPU、并行度三重可调 新项目(推荐Argon2id变种) 有些旧库不支持;迁移时注意兼容
我的选择

个人习惯:新项目首选Argon2id,因为它在抵御时序攻击和侧信道攻击上有专门设计。如果团队对参数不熟,就选bcrypt,社区成熟,库到处都有。别用pbkdf2,虽然也是慢哈希,但GPU优化后抵抗性一般。

代码示例:Python 中使用 bcrypt

嗯,这里要注意:永远不要自己实现密码哈希逻辑。用现成的库,它们把盐生成、哈希、成本控制都封装好了。

import bcrypt

# 注册时:生成盐,哈希密码
password = b"my_secure_password"
salt = bcrypt.gensalt(rounds=12)  # 成本因子12
hashed = bcrypt.hashpw(password, salt)
# 存储 hashed 到数据库(它已经包含盐和算法标识)

# 登录时:验证
stored_hash = b"$2b$12$..."  # 从数据库取出
user_input = b"my_secure_password"
if bcrypt.checkpw(user_input, stored_hash):
    print("密码正确")
else:
    print("密码错误")
曾经踩过的坑

我曾经在一次架构升级中,把bcrypt的成本因子从8直接改到12,结果线上登录响应时间从50ms飙到800ms。教训:成本因子调整要分批灰度,同时预计算新哈希逐步替换旧哈希,不能一次性全切。

完整密码存储流程

把加盐和慢哈希结合起来,流程就清楚了。下面我用一张图展示从注册到验证的完整链路。

密码存储流程:加盐 + 慢哈希 用户输入密码 生成随机盐 (16+字节) 拼接盐 + 密码 慢哈希计算 存储 盐 || 哈希值 注意 盐必须和哈希一起存储 验证时使用相同的盐+慢哈希

常见避坑清单

我踩过的坑,加上我帮别人修过的坑,列出来你对照自查。

  • 不要自己实现密码哈希函数——哪怕你自认为懂密码学,也大概率会弄错时长、编码或边界条件。用现成的库。
  • 不要用快哈希加一百万次迭代代替慢哈希——比如自己写 for(i=0;i<1000000;i++) { hash = SHA256(hash) },虽然慢了,但依然可以用GPU并行加速,本质还是快哈希。
  • 盐不要截断——我见过有人把随机盐截成4字节,说是“节省空间”。结果攻击者针对这个用户暴力枚举的时间大大缩短。
  • 成本因子要根据硬件性能调节——建议在目标服务器上测试,让一次哈希耗时在200~500ms。如果用户登录感觉卡顿,检查是不是成本太高。
  • 迁移旧哈希时要小心——如果旧系统用MD5存储,不要直接全量重算。采用“升级时验证通过后才写新哈希”的策略,逐步替换。
一句话总结

密码存储 = 随机盐 + 慢哈希。盐让彩虹表失效,慢哈希让暴力破解变慢。两个一起上,才能让你的用户睡得安稳。