bcrypt vs Argon2 vs scrypt:2026 密码哈希算法对比
一句话答案:2026 年新项目用 Argon2id,参数 m=19456, t=2, p=1。这是 OWASP 密码存储速查表给出的基线,目前抗 GPU 与抗侧信道能力最好的密码哈希算法就是它。
技术栈接不进 Argon2 时(罕见,但有些嵌入式或老旧运行时确实如此),退一步用 scrypt,参数 N=2^17, r=8, p=1。bcrypt 配合 cost=12 留给那些已经在用 bcrypt、又没法引入新依赖的遗留系统。如果硬性要求 FIPS-140 合规,那就 PBKDF2-HMAC-SHA-256,迭代 600,000 次。
| 算法 | OWASP 2026 参数 | 适用场景 |
|---|---|---|
| Argon2id | m=19456 KiB, t=2, p=1 | 新项目默认选择 |
| scrypt | N=2^17, r=8, p=1 | Argon2 不可用时 |
| bcrypt | cost=12(最低 10) | 仅遗留系统 |
| PBKDF2 | HMAC-SHA-256,600k 次迭代 | 必须 FIPS-140 合规 |
下面会解释这些数字怎么来的、如何按自己的硬件调参,以及怎么在不强制用户重置密码的情况下完成迁移。基准测试需要一批强度够的测试密码,可以用随机密码生成器生成。完整的安全清单见 Web 安全最佳实践指南。
密码哈希和通用哈希不是一回事
哈希函数从外面看都差不多:数据进去,定长摘要出来,无法逆推。但「给一个 4 GB 的 ISO 算哈希」和「给一个 12 字符密码算哈希」的目标完全相反。前者要尽硅片之所能地快,后者要尽登录延迟预算之所容地慢。
把这两件事混在一起做,就是数据库泄露最后变成账号失陷的常见原因。
MD5 和 SHA-256 为什么不够用
MD5、SHA-1、SHA-256 这类通用哈希是为吞吐量设计的。普通 CPU 上每秒处理几 GB 数据,GPU 上每秒能上几十 GB。做文件校验和、内容寻址很合适,拿来存密码就是灾难。
2024 年单卡 RTX 4090 跑 Hashcat 的数据:MD5 约 164 GH/s,SHA-256 约 22 GH/s。一个 8 字符的小写字母数字密码(36^8 ≈ 2.8 × 10^12 种候选),单 GPU 不到一分钟就能破 MD5,SHA-256 也只要几分钟。存 sha256(password) 的库被拖出来,基本等同于明文。
加 salt 也救不了。salt 能废掉预计算彩虹表,但拖不住针对单一账号的攻击:攻击者把候选密码和泄露的 salt 拼一下再哈希就行了。
非安全用途的校验和场景,MD5 和 SHA-256 还是有用——通用哈希生成器这类工具就是干这个的。何时用哪种算法的细节见 MD5 与 SHA-256 哈希算法对比。但密码场景,需要的是一个被刻意拖慢的哈希。
现代密码哈希需要的三个特性
2026 年值得部署的密码哈希要满足三件事:
- 设计上就慢,工作因子可调。单次登录控制在 100–500 ms——快到用户感觉不到,慢到离线攻击者每枚举一百万次就要烧掉好几天。工作因子要做成参数,硬件升级了好往上调。
- 每条记录独立 salt。每个密码配一条唯一随机 salt,彩虹表失效,攻击者只能挨个账号试。现代算法会自动生成 salt 并写进输出字符串。
- 内存困难(memory-hard)。GPU 和 ASIC 算力强,但高带宽内存贵。一个每次哈希要吃几十 MiB 的算法,会逼着攻击者按并行度配 RAM,GPU 农场的成本曲线就崩了。
bcrypt 满足 (1) 和 (2),但不满足 (3)。scrypt 是第一个三项都占齐的算法。Argon2 在它基础上做了细化,拿下密码哈希竞赛。下面逐个看。
三种算法的架构与取舍
bcrypt:基于 Blowfish 的时间困难
bcrypt 由 Niels Provos 和 David Mazières 在 1999 年为 OpenBSD 写的。它基于 Blowfish 密码,把昂贵的密钥设置阶段(叫「EksBlowfish」)重复跑 2^cost 次。可调参数只有一个:工作因子(cost factor,也叫「log rounds」),加一翻倍。cost=10 跑 1,024 次密钥调度,cost=14 跑 16,384 次。
一个 bcrypt 哈希长这样:
$2b$12$R9h/cIPz0gi.URNNX3kh2OPST9/PgBkqquzi.Ss7KIUgO2t0jWMUW
│ │ │ │
│ │ │ └─ 31 字符 base64 哈希
│ │ └─ 22 字符 base64 salt
│ └─ cost factor (12)
└─ 算法标识符($2b$ = bcrypt v2)
格式自描述:verify() 从存储字符串里就能读出 cost 和 salt,不需要额外字段。
缺点也是真的。bcrypt 内存占用约 4 KiB,一张高端 GPU 能并行跑几千上万个 bcrypt 核。bcrypt 还会在 72 字节处静默截断输入——一个 100 字符的 passphrase,安全强度只等于前 72 字节。最大 cost 是 31,但超过 16 之后,普通硬件上的登录延迟就开始顶不住了。
scrypt:开了内存困难的头
scrypt 由 Colin Percival 在 2009 年为 Tarsnap 备份服务发布,2016 年成为标准 RFC 7914。它引入了内存困难(memory-hardness):算法用伪随机数据填满一大块缓冲区,再从随机位置读取,逼着任何实现都得真分配这块内存。
scrypt 有三个参数:
- N:CPU/内存代价(必须是 2 的幂)
- r:块大小,字节数(同时影响内存与混合轮数)
- p:并行度(独立计算数,主要用来在不增加内存的前提下增加 CPU 时间)
内存使用约 128 × N × r 字节。OWASP 推荐的 N=2^17, r=8 对应 128 × 131072 × 8 = 134,217,728 字节,也就是每次哈希 128 MiB。
scrypt 也是密钥派生函数,不光是密码哈希。加密货币钱包、全盘加密、最早的莱特币工作量证明都用过。一个项目要同时做密码存储和密钥派生时,这种双重身份用起来方便。
Argon2(id/i/d):密码哈希竞赛冠军
密码哈希竞赛(Password Hashing Competition)从 2013 年开到 2015 年,24 个候选算法围绕内存困难、抗侧信道、实现简洁三方面评比,Argon2 胜出。2021 年成为标准 RFC 9106。
Argon2 有三个变体,差别在混合阶段的内存寻址方式:
- Argon2d 用数据相关(data-dependent)寻址。对 GPU 和 ASIC 攻击抵抗最强,但会通过缓存时序侧信道泄露信息。适合加密货币工作量证明,不适合身份验证。
- Argon2i 用数据无关(data-independent)寻址。抗侧信道,但对 GPU 时空折中攻击的抵抗稍弱。
- Argon2id 是混合的:第一次遍历的前半段用 Argon2i 寻址(抗侧信道),其余部分用 Argon2d 寻址(抗 GPU)。RFC 9106 推荐 Argon2id 做密码哈希,OWASP 也一样。
Argon2 三个参数:
- m:内存,单位 KiB
- t:时间代价(对内存缓冲区的遍历次数)
- p:并行度(并发处理的 lane 数)
Argon2id 哈希用 PHC 字符串格式,样子是:
$argon2id$v=19$m=19456,t=2,p=1$c29tZXNhbHQ$RdescudvJCsgt3ub+b+dWRWJTmaaJObG
和 bcrypt 一样,所有参数都写在字符串里,verify() 不需要额外的参数表。
OWASP 2026 推荐参数
OWASP 密码存储速查表 是权威参考。下面的数字和它的当前指南一致,取值偏保守,按典型 Web 服务器、登录延迟预算 100–500 ms 定的。上线前最好还是在自己的硬件上跑基准。
Argon2id 参数:首选
OWASP 基线推荐:m=19456 (19 MiB), t=2, p=1。
服务器内存余量更充足的话,可以在内存与时间之间转移工作量。RFC 9106 给出了等效配置档,OWASP 接受其中任一组合:
| memoryCost (m) | timeCost (t) | parallelism (p) | 每次哈希 RAM |
|---|---|---|---|
| 47104 | 1 | 1 | 46 MiB |
| 19456 | 2 | 1 | 19 MiB(基线) |
| 12288 | 3 | 1 | 12 MiB |
| 9216 | 4 | 1 | 9 MiB |
| 7168 | 5 | 1 | 7 MiB |
调参经验。先按峰值并发登录的 RAM 预算确定 m。比如同时 100 路登录、4 GiB 余量,那就是每次哈希 40 MiB。然后把 t 调高,直到生产 CPU 上单次 verify 落在 100–500 ms。除非确实有多核理由,否则 p=1 别动(多数 Web 框架已经为每个请求分配了独立线程)。
scrypt 参数:Argon2 不可用时
OWASP 推荐:N=2^17 (131072), r=8, p=1,每次哈希 128 MiB。
如果每路并发 128 MiB 对你的服务器太重,OWASP 也允许更弱的配置:
| N | r | p | 每次哈希 RAM |
|---|---|---|---|
| 2^17 | 8 | 1 | 128 MiB(优选) |
| 2^16 | 8 | 1 | 64 MiB |
| 2^15 | 8 | 1 | 32 MiB |
N 必须是 2 的幂。r 增大会按比例同时增加内存和 CPU 工作量;p 增大只加 CPU 工作量,不加单实例内存。密码哈希场景里,r 和 p 留默认,只调 N。
bcrypt:仅限遗留系统,cost ≥ 10
OWASP 已经不再向新项目推荐 bcrypt,但它在 Devise、Spring Security、ASP.NET Identity,还有一堆自研认证系统里仍然是默认选择。
非用 bcrypt 不可时,规则是:
- cost factor 最低 10。低于 10,单张 GPU 几天内就能跑完一个泄露库。
- 推荐 12 到 14,看硬件。2024 年的现代 x86 服务器上,
cost=12每次哈希约 250 ms,cost=13约 500 ms。 - 生产硬件上目标 每次 verify 100–300 ms。要测,别猜。
- 记住 72 字节输入限制。允许 passphrase 的话,先用 SHA-256 预哈希(详见 FAQ)。
bcrypt 抗 GPU 能力被 4 KiB 内存占用卡住,cost 调多高都不可能赶上 Argon2id 的内存困难性。能上 Argon2id 就上 Argon2id。
一个参考:2024 年的 EPYC 服务器上,bcrypt(cost=12) 约 250 ms;高端笔记本接近 350 ms。如果实测和 100–500 ms 差出一个数量级,先看看你的库跑的到底是原生 bcrypt,还是退化到了某个慢吞吞的 JavaScript polyfill(一些 bundler 在 serverless 构建里会把原生依赖剥掉)。
PBKDF2:留给 FIPS-140 合规
PBKDF2(RFC 8018)在安全建议里是最后的兜底。它比 bcrypt 还老,不是内存困难的,GPU 攻击面前比上面三个倒得都快。但它是唯一通过 FIPS-140 验证的密码哈希原语,对联邦政府、HIPAA 医疗、部分金融部署来说这就是硬条件。
要用 PBKDF2 的话:
- PRF 用 HMAC-SHA-256(不要用 SHA-1,也不要用没有 HMAC 包装的纯 SHA-256)
- 至少 600,000 次迭代(OWASP 2026 基线)
- 每个密码至少 16 字节随机 salt
FIPS 不适用就选 Argon2id。PBKDF2 固定输出、固定内存的设计,意味着攻击者每多花一块钱在 GPU 上,就直接换来更多的密码尝试速率。
NIST 的 SP 800-63B 把 PBKDF2-HMAC 列为「approved」用于密码哈希,但没说它比内存困难替代更好。可以这么理解:NIST 允许 PBKDF2,是因为淘汰它会让所有遗留政府部署当场失效,不是因为它对绿地项目最优。
选型:到底用哪个
对比表
| 维度 | bcrypt | scrypt | Argon2id | PBKDF2 |
|---|---|---|---|---|
| 内存困难 | 否 | 是 | 是 | 否 |
| 抗 GPU | 中 | 高 | 极高 | 低 |
| 抗侧信道 | 中 | 中 | 高(id) | 中 |
| 参数复杂度 | 1(cost) | 3(N、r、p) | 3(m、t、p) | 1(iterations) |
| 库生态成熟度 | 极好 | 好 | 好 | 极好 |
| 输入长度限制 | 72 字节 | 无 | 无 | 无 |
| 标准化 | 事实标准 | RFC 7914 | RFC 9106 | RFC 8018 |
| OWASP 2026 状态 | 仅遗留 | 备选 | 首选 | 仅 FIPS |
默认用 Argon2id
新项目(常见 Web 应用,现代 Node/Python/Go/Rust/JVM 技术栈,没有 FIPS 约束)就用 Argon2id,参数 m=19456, t=2, p=1。能拿到目前最强的抗 GPU 和抗侧信道能力,参数嵌在哈希里(对库升级友好),没有输入长度陷阱。库生态也成熟:npm 上的 argon2、PyPI 上的 argon2-cffi、golang.org/x/crypto/argon2、crates.io 上的 argon2 crate,都有人在维护和跑基准。
何时改用 scrypt 或 bcrypt
选 scrypt 的情况:你的运行时上不了 Argon2(2026 年确实少见,连 Cloudflare Workers 和 Deno 现在都能跑);或者生产已经基于 scrypt 部署,迁移成本高于安全收益。scrypt 仍然是个稳健的算法,就是少了 Argon2id 的抗侧信道精修。
选 bcrypt 的情况:在维护遗留系统,依赖最小化要求很硬(不能用原生代码、不能加包),用户群也能接受 72 字节输入限制。bcrypt 在互联网规模部署了二十年,失效模式研究得很透。
选 PBKDF2 的情况:监管要求如此。就这一个理由。审计员认可 Argon2id 的话(非 FIPS 工作负载下越来越多),那就上 Argon2id。
常见错误
过去十年的密码存储事故,绝大多数都能追溯到一小撮反复出现的工程错误。这些坑都不算冷门,把下面这张清单贴屏幕上,对着自己的认证代码逐项过一遍,基本都能挖出来。
- 直接用 SHA-256 或 MD5 哈希密码。这是密码存储事故的头号原因。原因见 MD5 与 SHA-256 对比。
- 所有用户共用一个全局 salt。salt 必须每条记录唯一。Argon2 和 bcrypt 会自动生成,别去覆盖。
- 把哈希时间设到 50 ms 以下。这是拿安全性换一个用户根本感觉不到的速度提升。目标 100–500 ms。
- 把哈希时间设到 1 秒以上。这给登录端点制造了一个拒绝服务向量。封顶约 500 ms。
- 在客户端哈希密码再把摘要发到服务端。摘要现在就是新密码。任何拿到数据库的人都能直接登录,连逆推都省了。哈希在服务端做。
- 把算法参数另存一列。PHC 字符串格式已经把它们写进哈希了,直接用。
- 错误处理时打印密码或哈希。这两样都属于用户,不属于你的日志聚合系统。在请求解析层就脱敏,别让它们流进任何 logger。
- 把
verify()抛的异常当作认证失败。库对损坏的存储哈希抛异常,应该把这个错误暴露出来,而不是悄悄走到「密码错误」分支。把「密码错误」(返回 401)和「存储哈希损坏」(返回 500 并叫醒值班)分开。
落地实现
Node.js 用 Argon2id
argon2 包(参考实现的原生绑定)是 Node 上的标准选择:
import argon2 from 'argon2';
// 注册或修改密码时进行哈希
const hash = await argon2.hash(password, {
type: argon2.argon2id,
memoryCost: 19456, // 19 MiB
timeCost: 2,
parallelism: 1,
});
// → '$argon2id$v=19$m=19456,t=2,p=1$<salt>$<hash>'
// 登录时验证
const ok = await argon2.verify(hash, candidate);
if (!ok) throw new Error('Invalid credentials');
// 检测过期参数,登录成功后再哈希
if (argon2.needsRehash(hash, { type: argon2.argon2id, memoryCost: 19456, timeCost: 2, parallelism: 1 })) {
const upgraded = await argon2.hash(candidate, {
type: argon2.argon2id, memoryCost: 19456, timeCost: 2, parallelism: 1,
});
await db.users.update({ id: user.id }, { password_hash: upgraded });
}
needsRehash 这一步是长期迁移的关键:每次成功登录都顺便把存储哈希升级到当前参数,用户全程没感知。
Python 同样的写法,用 argon2-cffi:
from argon2 import PasswordHasher
from argon2.exceptions import VerifyMismatchError
ph = PasswordHasher(memory_cost=19456, time_cost=2, parallelism=1)
# 哈希
stored = ph.hash(password)
# 验证
try:
ph.verify(stored, candidate)
except VerifyMismatchError:
raise ValueError('Invalid credentials')
# 参数升级时再哈希
if ph.check_needs_rehash(stored):
stored = ph.hash(candidate)
Go 用 golang.org/x/crypto/argon2:
import (
"crypto/rand"
"golang.org/x/crypto/argon2"
)
func hashPassword(password string) ([]byte, []byte) {
salt := make([]byte, 16)
rand.Read(salt)
hash := argon2.IDKey([]byte(password), salt, 2, 19456, 1, 32)
return hash, salt
}
Go 标准库不带 PHC 格式编码器;直接用 argon2.IDKey 原语时,参数和 salt 怎么和哈希一起编码得自己处理。多数 Go 项目用 github.com/alexedwards/argon2id 这类封装来兜底。
Rust 用 argon2 crate,同样直白:
use argon2::{Argon2, PasswordHasher, PasswordVerifier, password_hash::{SaltString, rand_core::OsRng}};
let salt = SaltString::generate(&mut OsRng);
let argon2 = Argon2::default(); // 默认即 Argon2id, m=19456, t=2, p=1
let hash = argon2.hash_password(password.as_bytes(), &salt)?.to_string();
// 验证
let parsed = argon2::password_hash::PasswordHash::new(&hash)?;
argon2.verify_password(candidate.as_bytes(), &parsed)?;
这三种运行时之间生成的字符串可互换:Node 创建的哈希,Python 或 Rust 都能干净通过验证。这种跨运行时兼容性,在多语言架构里让 Argon2 比那些算法专属封装好用得多。
从 bcrypt 迁到 Argon2id 的做法
基本不会有清空用户表从头来一遍的机会。正确的迁移做法和 hash 生成器 FAQ 里的 MD5 转 bcrypt 段落一致:登录驱动的软迁移。
加一列记录算法:
ALTER TABLE users ADD COLUMN password_algo VARCHAR(16) NOT NULL DEFAULT 'bcrypt';
登录时按算法分发到对应验证器:
async function verifyAndMaybeRehash(user, candidate) {
let ok;
if (user.password_algo === 'argon2id') {
ok = await argon2.verify(user.password_hash, candidate);
} else if (user.password_algo === 'bcrypt') {
ok = await bcrypt.compare(candidate, user.password_hash);
if (ok) {
// 旧算法验证通过 → 立即用 Argon2id 重新哈希
const newHash = await argon2.hash(candidate, {
type: argon2.argon2id, memoryCost: 19456, timeCost: 2, parallelism: 1,
});
await db.users.update({ id: user.id }, {
password_hash: newHash,
password_algo: 'argon2id',
});
}
}
return ok;
}
设一个 6–12 个月的日落窗口。第 9 个月发一封「您的密码用过时方式存储,登录一次即可升级」邮件。12 个月后,仍在 bcrypt 的账户下次登录时强制重置密码。活跃用户透明迁移,不活跃账户经历一次性的摩擦。
从 scrypt 或 PBKDF2 迁出也是同一套做法,需要的状态只有 password_algo 一列。
Pepper、长度限制和编码陷阱
几个真实部署里常踩的边角:
Pepper。pepper 是应用级密钥,哈希前和每个密码混合,跟数据库分开放(KMS、env var、Hashicorp Vault 之类)。数据库泄露但应用密钥没泄露的话,泄露的哈希拿不到 pepper 就攻不动。施加方式用 HMAC,不要拼接:
import { createHmac } from 'crypto';
const peppered = createHmac('sha256', process.env.PEPPER).update(password).digest();
const hash = await argon2.hash(peppered, { type: argon2.argon2id, /* ... */ });
Pepper 别频繁轮转(轮转得重新哈希),但要支持版本化轮转:PEPPER_V2,验证时回退到 PEPPER_V1。
bcrypt 72 字节限制。非用 bcrypt 又想支持任意长度密码,先用 SHA-256 预哈希再 base64 编码(避开嵌入的 NUL 字节,bcrypt 对 NUL 的处理也不一致):
import { createHash } from 'crypto';
const prepped = createHash('sha256').update(password, 'utf8').digest('base64');
const hash = await bcrypt.hash(prepped, 12);
验证时也得执行同样的 prepped 变换。在认证代码里加一段醒目注释,半年后回来看时会很庆幸。
UTF-8 归一化。字符串 "café" 可以编成 c-a-f-é(4 个码点,NFC),也可以编成 c-a-f-e + 组合用尖音符(5 个码点,NFD)。看起来一模一样,哈希结果不同。哈希前统一归一化到 NFC:
const normalized = password.normalize('NFC');
移动键盘和从 PDF 复制粘贴触发这个坑的频次,比想象中高得多。
别在客户端预哈希。客户端算出来发到服务端的哈希,就是新密码。任何能读到数据库的人都能直接过认证。哈希在服务端做,没有例外。JWT 也改变不了这点;JWT 能认证什么、不能认证什么,见如何解码 JWT Token。
在生产硬件上跑基准,不要拿笔记本测。一台 13 代 Intel 笔记本跑 Argon2id m=19456, t=2, p=1 约 35 ms。同样参数在 t3.small EC2 实例上接近 180 ms,在 Raspberry Pi 4 上超过 600 ms。挑出真正会跑生产的硬件,跑 1,000 次 verify,按中位数调参。serverless 容器冷启动带来的登录延迟波动也值得测一下——Lambda 冷启动可能跟哈希无关地多出 200–800 ms。
FAQ
密码哈希和加密有什么区别?
哈希是单向的:算出一个定长指纹,无法逆推回输入。加密是双向的:有正确的密钥就能解密回原文。密码要哈希,不要加密。服务端不应该有能力恢复任何用户的密码——这样数据库泄露才不会变成凭据泄露。
为什么不能直接用 SHA-256 哈希密码?
SHA-256 是为速度设计的。一张现代 GPU 每秒能算 220 亿次 SHA-256,泄露库里一个 8 字符小写密码几分钟就会失守。密码哈希需要 SHA-256 没有的三件事:刻意慢的执行、每条记录独立 salt、内存困难。这套权衡和 hash 生成器里「不要用 MD5 做安全用途」的提示一致;攻击者如何把弱哈希还原成明文,可参阅密码熵详解。
bcrypt 2026 年还安全吗?
bcrypt 本身没被攻破,基于 Blowfish 的密钥调度在密码学上仍然成立。变的是威胁模型:GPU 和 ASIC 让 bcrypt 没有内存困难这件事,相对 Argon2id 成了明显弱点。OWASP 2026 的态度是:cost ≥ 10 的 bcrypt 在遗留系统里可以接受,新项目用 Argon2id。
Argon2i、Argon2d、Argon2id 用哪个?
用 Argon2id。RFC 9106 指定它为密码哈希的推荐变体。Argon2i 是数据无关的(抗侧信道但对 GPU 时空折中弱),Argon2d 是数据相关的(抗 GPU 但易受缓存时序侧信道攻击),Argon2id 是混合体,一份成本拿到两个属性。
我的应用要怎么选 Argon2id 参数?
从 OWASP 基线起步:m=19456, t=2, p=1。然后在生产 CPU 上做基准并调整:
- 定下每路登录的 RAM 预算(比如峰值并发下 50 MiB)。
m设到该值或更低。- 在循环里跑
argon2.hash(),测墙钟时间。 t往上调,直到中位耗时落在 100 到 500 ms 之间。
除非已经 profile 过,确认多 lane 并行对你的运行时有帮助,否则 p=1 别动。高流量认证服务器一般偏向更高的 t 加更低的 m,能腾出更多 RAM 余量。
bcrypt 的 72 字节限制是什么?长 passphrase 怎么办?
bcrypt 把输入喂给 Blowfish 密钥调度,后者在 72 字节处截断。150 字符的 passphrase 安全度只等于前 72 字节,后面忽略掉。处理办法:先用 SHA-256(32 字节)或 SHA-512(64 字节)预哈希,摘要 base64 编码以避开 NUL,再喂给 bcrypt。Argon2id 和 scrypt 没这个限制,直接接受任意长度输入。
能不能在不强制重置密码的情况下从 bcrypt 迁到 Argon2?
可以。做法:password_algo 列后面同时存两种算法,验证时分发到对应库;每次 bcrypt 验证成功后,立刻用 Argon2id 重新哈希并更新该行。活跃用户在正常登录节奏里静默迁移完成。给不活跃账户设一个 6–12 个月的日落窗口,到期后还在 bcrypt 的记录强制重置密码。算法到算法的迁移都是这一套。
PBKDF2 在 2026 年还是好选择吗?
只有 FIPS-140 合规逼你这么做时是。典型场景是联邦政府、受监管的医疗(HIPAA)、部分金融系统。PRF 用 HMAC-SHA-256,迭代至少 600,000 次。PBKDF2 不是内存困难的,同等延迟预算下比 Argon2id 更快地倒在 GPU 攻击面前。FIPS 不适用就选 Argon2id,省掉这套合规折腾。
2026 年的密码哈希答案很短:默认 Argon2id 配 OWASP 基线参数;Argon2 用不了就退回 scrypt;bcrypt 只在遗留系统逼你时保留;PBKDF2 留给 FIPS 约束系统。再配上每条记录独立的 salt(现代库都自动处理)、存在数据库之外的应用级 pepper,以及一个登录驱动的再哈希循环,硬件升级时就能顺着把工作因子一路调上去。
用随机密码生成器生成一组有代表性的密码集,在生产 CPU 上跑 verify 路径的基准,把参数写进一个常量文件,这样 2028 年下一位工程师要拧哪颗螺丝一目了然。完整的安全上下文(TLS、会话管理、限速、MFA)都在 Web 安全最佳实践指南里。给应用选对哈希。