密码存储方案对比:明文、哈希、加盐哈希、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 就足够。

核心知识框架

下面这张图总结了四种方案的安全层级与关系,你看一眼心里就有谱了。

密码存储方案安全层级对比 用户密码 明文存储 纯哈希 加盐哈希 bcrypt 直接泄露 ❌ 高危 彩虹表可破 ⚠️ 低安全 抗彩虹表 🐢 中安全 慢哈希+盐 ✅ 高安全 从左到右:安全强度递增,实现成本也递增 注意:纯哈希+快速算法不是密码哈希专用函数 bcrypt/scrypt/Argon2 才是设计给你的 ✅ 首选 bcrypt,进阶用 Argon2

实践建议

总结一下我的几个核心原则:

  • 绝不要存明文。不管是开发环境还是测试库,都不行。我用过一个内部工具,开发库的密码就是明文——有人开玩笑说,这等于在办公室里贴满了所有人的银行卡密码。
  • 不要自己发明加密方案。比如把密码用 AES 加密然后存密文,密钥写在配置文件里——这比明文好不到哪去。
  • 优先用 bcrypt。PyPI 直接 pip install bcrypt,一行代码就能用。成本因子设 12,在普通服务器上大约耗时 150ms,用户体验完全可以接受。
  • 如果用户量极大,考虑把成本因子降到 10。但记住:安全永远是数据库泄露后的最后防线,别为了省 100ms 让用户数据陷入风险。

我个人项目里,只要涉及用户密码,第一反应就是 bcrypt。它已经被检验了十几年,是经过实战考验的“老兵”。你想想看,哪天你数据库真的被拖了,至少你不会成为技术圈的笑柄——“那个用 MD5 存密码的兄弟又翻车了”。