
哈希 vs 加密:关键区别
在选择算法之前,先理解你实际在做什么。
加密是可逆的——你可以用正确的密钥将密文解密回明文。加密用于保护日后需要读取的数据:信用卡号、医疗记录、私密消息。
哈希是单向的——哈希函数接收输入并产生固定大小的输出(哈希值或摘要),没有算法可以逆转它。你无法从哈希值回到原始密码。
为什么单向更适合密码? 因为你不需要知道用户的密码——你只需要验证它。当用户登录时,你对他们输入的内容进行哈希,并与存储的哈希值比较。如果匹配,则密码匹配。实际密码永远不需要存储或恢复。
如果有人窃取了你的数据库,他们得到的是哈希值——而不是密码。他们必须逐个破解每个哈希值。

为什么不直接用 SHA-256?
SHA-256 是一种为速度设计的加密哈希函数。在现代硬件上,尤其是 GPU 加速下,它每秒可以计算数十亿次哈希。这对于验证文件完整性很好。但对于密码存储来说是灾难性的。
攻击者拿到泄露的 SHA-256 密码哈希数据库后,可以以惊人的速度运行字典攻击和彩虹表攻击:
- 现代 GPU 每秒可计算约 100 亿次 SHA-256 哈希
- 一个 8 字符的小写字母加数字密码约有 2.8 万亿种组合
- 以每秒 100 亿次计算,破解所有组合大约需要 4.6 分钟
SHA-256 明确不是为密码哈希设计的。将其用于密码是一个众所周知的安全错误。
同样适用于 MD5(更快,且因其他原因已被破解)以及原始的 SHA-1/SHA-512。
好的密码哈希算法应具备什么?
好的密码哈希算法被设计为慢——故意地、可调地,并且以对防御者比对攻击者更有利的方式。
关键属性:
设计上慢——每次哈希在你的硬件上应耗时 100–300 毫秒。对合法用户登录来说很快;当攻击者需要尝试数百万次猜测时则极其缓慢。
工作因子可调——随着硬件速度提升,你需要增加成本。好的算法允许你调整成本参数。
包含盐值——在每个哈希计算前混入一个随机值,每个密码唯一。这可以防止彩虹表攻击(预计算哈希表),并确保两个相同密码的用户得到不同的哈希值。
内存硬(理想)——某些算法每次计算需要大量 RAM。GPU 有很多核心,但每个核心的内存带宽有限,因此内存硬算法特别能抵抗 GPU 破解。
算法

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');

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 密码哈希数据库,无需强制重置密码即可迁移:
- 添加新列
password_hash_v2(可空) - 成功登录时(你拥有明文密码):计算 bcrypt/Argon2 哈希,存入 v2
- 登录时先检查 v2;如果 v2 为空则回退到 v1
- 90 天后,对仍在使用 v1 的账户强制重置密码
- 删除 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 工具 在浏览器中哈希和验证密码——用于测试成本因子和验证现有哈希值。