正在加载,请稍候…

密码哈希详解:bcrypt、SHA-256、Argon2 及实际选择

理解密码哈希为何不同于加密,bcrypt、SHA-256、PBKDF2 和 Argon2 的工作原理,以及 2026 年安全存储密码应选哪种算法。

密码哈希详解:bcrypt、SHA-256、Argon2 及实际选择

哈希 vs 加密:关键区别

在选择算法之前,先理解你实际在做什么。

加密是可逆的——你可以用正确的密钥将密文解密回明文。加密用于保护日后需要读取的数据:信用卡号、医疗记录、私密消息。

哈希是单向的——哈希函数接收输入并产生固定大小的输出(哈希值或摘要),没有算法可以逆转它。你无法从哈希值回到原始密码。

为什么单向更适合密码? 因为你不需要知道用户的密码——你只需要验证它。当用户登录时,你对他们输入的内容进行哈希,并与存储的哈希值比较。如果匹配,则密码匹配。实际密码永远不需要存储或恢复。

如果有人窃取了你的数据库,他们得到的是哈希值——而不是密码。他们必须逐个破解每个哈希值。

密码哈希详解:bcrypt、SHA-256、Argon2 及实际选择 插图

为什么不直接用 SHA-256?

SHA-256 是一种为速度设计的加密哈希函数。在现代硬件上,尤其是 GPU 加速下,它每秒可以计算数十亿次哈希。这对于验证文件完整性很好。但对于密码存储来说是灾难性的。

攻击者拿到泄露的 SHA-256 密码哈希数据库后,可以以惊人的速度运行字典攻击和彩虹表攻击:

  • 现代 GPU 每秒可计算约 100 亿次 SHA-256 哈希
  • 一个 8 字符的小写字母加数字密码约有 2.8 万亿种组合
  • 以每秒 100 亿次计算,破解所有组合大约需要 4.6 分钟

SHA-256 明确不是为密码哈希设计的。将其用于密码是一个众所周知的安全错误。

同样适用于 MD5(更快,且因其他原因已被破解)以及原始的 SHA-1/SHA-512。

好的密码哈希算法应具备什么?

好的密码哈希算法被设计为慢——故意地、可调地,并且以对防御者比对攻击者更有利的方式。

关键属性:

  1. 设计上慢——每次哈希在你的硬件上应耗时 100–300 毫秒。对合法用户登录来说很快;当攻击者需要尝试数百万次猜测时则极其缓慢。

  2. 工作因子可调——随着硬件速度提升,你需要增加成本。好的算法允许你调整成本参数。

  3. 包含盐值——在每个哈希计算前混入一个随机值,每个密码唯一。这可以防止彩虹表攻击(预计算哈希表),并确保两个相同密码的用户得到不同的哈希值。

  4. 内存硬(理想)——某些算法每次计算需要大量 RAM。GPU 有很多核心,但每个核心的内存带宽有限,因此内存硬算法特别能抵抗 GPU 破解。

算法

密码哈希详解:bcrypt、SHA-256、Argon2 及实际选择 插图

bcrypt

1999 年专门为密码哈希设计。至今仍是全球部署最广泛的密码哈希算法。

工作原理: 接收一个密码和一个随机的 16 字节盐值。使用 Blowfish 密码的密钥调度(已知开销大)进行迭代。成本参数 N 表示算法运行 2^N 轮。

$2b$12$LQv3c1yqBWVHxkd0LHAkCOYz6TtxMQJqhN8/lfz3Fkxtu6RNWz9tK
 ^^  ^^                              
 |   成本因子(12 = 2^12 = 4096 轮)
 版本标识符

成本因子指南:

成本 现代服务器上大致时间 使用场景
10 ~100ms 生产环境最低要求
12 ~300ms 推荐默认值
14 ~1 秒 高安全性账户
16 ~4 秒 管理员账户、金融账户

限制: bcrypt 将密码截断至 72 字节。超过 72 个字符的密码会被静默截断。这在实践中很少影响,但值得了解。

// Node.js
const bcrypt = require('bcrypt');
const saltRounds = 12;

// 哈希
const hash = await bcrypt.hash(plainTextPassword, saltRounds);

// 验证
const match = await bcrypt.compare(plainTextPassword, storedHash);
# Python
import bcrypt

# 哈希
password = b'mysecretpassword'
hashed = bcrypt.hashpw(password, bcrypt.gensalt(rounds=12))

# 验证
bcrypt.checkpw(password, hashed)  # True 或 False

PBKDF2

基于密码的密钥派生函数 2。定义于 RFC 8018。内置于许多标准库和合规框架(NIST 批准、FIPS 140 兼容)。

工作原理: 重复应用伪随机函数(通常是 HMAC-SHA256 或 HMAC-SHA512),以迭代次数作为工作因子。

迭代次数指南(2026):

  • OWASP 推荐:600,000 次迭代,使用 HMAC-SHA256
  • NIST SP 800-63B 推荐:至少 10,000(较旧指南,建议更高)
import hashlib
import os

# 哈希
salt = os.urandom(32)
key = hashlib.pbkdf2_hmac('sha256', password.encode(), salt, 600000)

# 存储:salt + key(均以十六进制或字节形式)

PBKDF2 的弱点:它不是内存硬的。GPU 可以高效地并行化它。在等效速度设置下,bcrypt 和 Argon2 更难用专用硬件破解。

Argon2

密码哈希竞赛(2015)的获胜者。当前的金标准,OWASP 推荐用于新系统。

三种变体:

  • Argon2d — 最大化抵抗 GPU 破解,易受侧信道攻击。用于加密货币。
  • Argon2i — 抵抗侧信道攻击。用于大多数应用中的密码哈希。
  • Argon2id — 两者的混合。推荐默认值。

可配置参数:

  • 内存成本 (m) — RAM 使用量,以千字节为单位。最低 64 MB,推荐 128 MB 以上。
  • 时间成本 (t) — 迭代次数。从 3 开始。
  • 并行度 (p) — 并行线程数。匹配服务器的 CPU 核心数。
# Python (argon2-cffi)
from argon2 import PasswordHasher

ph = PasswordHasher(time_cost=3, memory_cost=131072, parallelism=4)  # 128 MB

# 哈希
hash = ph.hash("mysecretpassword")

# 验证
try:
    ph.verify(hash, "mysecretpassword")  # 返回 True
except Exception:
    pass  # 密码错误
// Node.js (argon2)
const argon2 = require('argon2');

const hash = await argon2.hash('mysecretpassword', {
  type: argon2.argon2id,
  memoryCost: 131072,  // 128 MB
  timeCost: 3,
  parallelism: 4,
});

const valid = await argon2.verify(hash, 'mysecretpassword');

密码哈希详解:bcrypt、SHA-256、Argon2 及实际选择 插图

scrypt

由 Colin Percival 于 2009 年设计。在 Argon2 出现之前就是内存硬的。至今仍被广泛使用且安全。

参数:N(CPU/内存成本)、r(块大小)、p(并行度)。OWASP 推荐:最低 N=65536、r=8、p=1。

const crypto = require('crypto');
const salt = crypto.randomBytes(32);

crypto.scrypt('password', salt, 64, { N: 65536, r: 8, p: 1 }, (err, derivedKey) => {
  // derivedKey 是哈希值
});

应该使用哪种算法?

场景 推荐
新应用(2026) Argon2id
需要 FIPS 合规的平台 PBKDF2 配合 HMAC-SHA512,60 万次迭代
为现有系统添加密码哈希 bcrypt(广泛支持,久经考验)
从 MD5/SHA1 迁移 上述任意一种——立即执行
需要每秒哈希大量密码 调低成本(不低于安全阈值)

绝不使用: MD5、SHA-1、SHA-256、SHA-512、明文或未加盐的密码存储。

从不安全哈希迁移

如果你有一个 MD5 或 SHA-1 密码哈希数据库,无需强制重置密码即可迁移:

  1. 添加新列 password_hash_v2(可空)
  2. 成功登录时(你拥有明文密码):计算 bcrypt/Argon2 哈希,存入 v2
  3. 登录时先检查 v2;如果 v2 为空则回退到 v1
  4. 90 天后,对仍在使用 v1 的账户强制重置密码
  5. 删除 v1 列

这样你可以静默升级活跃用户,并通过强制重置处理其余用户。

常见错误

存储明文密码 — 最灾难性的错误。根据公开的泄露报告,约 30% 的被入侵公司犯此错误。

使用快速哈希函数 — MD5、SHA-1、SHA-256 用于密码存储。快在这里是坏事。

忘记加盐 — 未加盐的 bcrypt 弱得多。所有好的库会自动处理加盐——不要手动实现。

所有密码使用相同盐值 — 盐值必须每个密码唯一,每次随机生成。

登录时不重新哈希 — 如果你随时间增加了成本因子,在用户成功登录时用新成本因子重新哈希密码。许多库通过“需要重新哈希”检查来处理。

// bcrypt:如果成本因子改变则重新哈希
const currentHash = getHashFromDB(userId);
if (await bcrypt.compare(password, currentHash)) {
  if (bcrypt.getRounds(currentHash) < TARGET_ROUNDS) {
    const newHash = await bcrypt.hash(password, TARGET_ROUNDS);
    updateHashInDB(userId, newHash);
  }
  // 登录成功
}

→ 使用 Bcrypt 工具 在浏览器中哈希和验证密码——用于测试成本因子和验证现有哈希值。