密码存储方案对比:明文、哈希、加盐哈希、bcrypt 等
好,咱们直接进入正题。密码怎么存,是每个后端工程师都躲不过的坎。我刚入行时看过一个项目,密码直接明文怼数据库——嗯,那项目后来被脱裤了,数据被公开“游街”。你别笑,真有公司这么干。今天我就把这几种方案掰开揉碎了讲清楚,你想想看,哪种存法才能让你睡得着觉?
明文存储——最危险的做法
说白了,就是把用户密码原封不动写进数据库。比如用户注册时输入 hello123,库里的 password 字段就是 hello123。
我曾经帮一个创业团队做安全审计,发现他们的管理后台居然能把所有用户密码以明文形式导出成 CSV。我跟他们说:这相当于你把家里的钥匙直接挂在门口,路人随便拿。后来他们花了三天重写了认证模块。
明文存储的问题显而易见:
- 数据库一旦泄露,所有密码直接曝光
- 内部员工可以随意查看用户密码
- 用户如果跨平台用同一个密码,其他账户也跟着完蛋
这种方案唯一的“优点”就是查询快、实现简单。但用我同事的话说:这是用命换速度,不值当。
哈希存储——进步但不够
咱们都知道,哈希函数是单向的。传进 hello123,输出一串看起来像乱码的东西,比如 aaf4c61ddcc5e8a2dabede0f3b482cd9aea9434d。数据库只存哈希值,登录时再把输入的密码哈希一下对比就行。
import hashlib
def store_password(password: str) -> str:
return hashlib.sha1(password.encode()).hexdigest()
# 存储哈希
hashed = store_password("hello123")
print(hashed) # aaf4c61ddcc5e8a2dabede0f3b482cd9aea9434d
我个人习惯把这种方案叫「半吊子安全」。为啥?因为你用 MD5 或者 SHA-1,攻击者建个彩虹表就能快速反查。我在项目中见过有人用 MD5 存密码,还觉得自己挺专业……结果被彩虹表三分钟戳穿。
哈希存储的致命弱点有两个:
- 相同密码产生相同哈希,攻击者可以预计算
- 哈希速度太快,暴力破解效率高(比如 SHA-256 每秒能算上亿次)
一句话:纯哈希 ≈ 给锁配了把塑料钥匙,看着像锁了,实际一敲就碎。
加盐哈希——解决彩虹表问题
那怎么办?加盐。盐是一个随机字符串,每个用户都不一样。存的时候把盐和密码拼在一起算哈希,库里的记录是 盐 + 哈希值。
import os, hashlib
def salted_hash(password: str) -> tuple:
salt = os.urandom(16).hex()
hash_obj = hashlib.sha256((salt + password).encode())
return salt, hash_obj.hexdigest()
salt, hashed = salted_hash("hello123")
# 存储 (salt, hashed)
这样一来,就算两个用户密码相同,存进库里的哈希值也不一样。彩虹表就废了。
我记得有个朋友问我:盐用固定值行不行?我说那叫「腌了一缸咸菜」——一个盐破全破。盐必须每个用户随机生成,长度至少 16 字节,最好用 os.urandom。
加盐哈希解决了彩虹表,但还没解决哈希速度快的问题。攻击者拿到盐之后,对单一用户仍然可以高速暴力破解。所以这方案目前只算「及格」,离优秀还差一截。
bcrypt——现代标准
bcrypt 是我现在项目里的标配。它内置了盐,而且有个叫「工作因子」(cost factor)的参数,能控制哈希的计算时间。你调高成本,让每次计算耗时 100ms,攻击者暴力破解的效率就直接掉了几个数量级。
import bcrypt
def bcrypt_hash(password: str) -> bytes:
# 工作因子默认 12,建议 10~14
return bcrypt.hashpw(password.encode(), bcrypt.gensalt(rounds=12))
def check_password(password: str, stored_hash: bytes) -> bool:
return bcrypt.checkpw(password.encode(), stored_hash)
hashed = bcrypt_hash("hello123")
print(hashed)
# 输出类似:$2b$12$...
我建议后端项目直接上 bcrypt。Python 的 bcrypt 库、Ruby 的 bcrypt gem、Go 的 golang.org/x/crypto/bcrypt 都是成熟实现。如果遇到性能瓶颈,可以把成本因子降到 10,但别再低了。
bcrypt 还有个小设计:它会自动截断超过 72 字节的密码。这一点要留心,别让用户设超长密码然后发现没法验证。
其他方案简要对比
scrypt 和 Argon2 也是专门为密码哈希设计的函数。scrypt 增加了内存消耗,让专用硬件破解更贵;Argon2 是 2015 年密码哈希竞赛的冠军,目前最先进。但 bcrypt 因为生态成熟、兼容性好,在绝大多数场景下够用。
| 存储方式 | 安全性 | 性能 | 抗彩虹表 | 推荐度 |
|---|---|---|---|---|
| 明文 | 无 | 极快 | 否 | ❌ 不用 |
| 纯哈希 (MD5/SHA) | 低 | 极快 | 否 | ❌ 不推荐 |
| 加盐哈希 | 中 | 快 | 是 | ⚠️ 可接受但过时 |
| bcrypt | 高 | 可调慢 | 是 | ✅ 强烈推荐 |
| scrypt / Argon2 | 很高 | 可调慢且吃内存 | 是 | ✅ 进阶选择 |
从表格里能看到,bcrypt 在安全与易用性间取得了很好的平衡。如果你负责的项目用户的隐私等级高(比如金融、医疗),可以考虑上 Argon2。但中小项目用 bcrypt 就足够。
核心知识框架
下面这张图总结了四种方案的安全层级与关系,你看一眼心里就有谱了。
实践建议
总结一下我的几个核心原则:
- 绝不要存明文。不管是开发环境还是测试库,都不行。我用过一个内部工具,开发库的密码就是明文——有人开玩笑说,这等于在办公室里贴满了所有人的银行卡密码。
- 不要自己发明加密方案。比如把密码用 AES 加密然后存密文,密钥写在配置文件里——这比明文好不到哪去。
- 优先用 bcrypt。PyPI 直接 pip install bcrypt,一行代码就能用。成本因子设 12,在普通服务器上大约耗时 150ms,用户体验完全可以接受。
- 如果用户量极大,考虑把成本因子降到 10。但记住:安全永远是数据库泄露后的最后防线,别为了省 100ms 让用户数据陷入风险。
我个人项目里,只要涉及用户密码,第一反应就是 bcrypt。它已经被检验了十几年,是经过实战考验的“老兵”。你想想看,哪天你数据库真的被拖了,至少你不会成为技术圈的笑柄——“那个用 MD5 存密码的兄弟又翻车了”。