阅读提要
密码不能加密,只能哈希。但哈希函数选错、成本因子定错,等于给自己埋一颗定时炸弹——三年前的参数,今天可能已经不安全。
三条底线
不能加密。 加密可逆,密钥泄露等于密码全泄。密码只能用单向哈希。
不能裸哈希。 直接 sha256(password) 的问题是:同样的密码得到同样的哈希,攻击者可以用彩虹表批量比对。必须加盐,每个用户独立的随机盐。
不能自己实现。 密码哈希有专门的算法(bcrypt、scrypt、argon2),它们的共同特点是故意慢,并且可调。用通用哈希函数,等于给暴力破解提速。

为什么是 bcrypt
bcrypt 有三个特性:
- 内置加盐:每次哈希自动生成随机盐,存在结果里;
- 可调成本:成本因子每加 1,计算时间翻倍;
- 内存访问模式固定:不如 argon2 抗 GPU,但实现简单、库成熟。
对普通站点足够。如果要从头选,argon2id 更现代;但 bcrypt 兼容性最好,bcryptjs 这类纯 JS 实现也能用(速度慢一些,对服务端反而无所谓)。
成本因子怎么定
这是最容易被拍脑袋定错的地方:很多人抄一个 10 就再也没改过。
正确的定法只有一个:测量。
import bcrypt from 'bcryptjs'
for (const rounds of [10, 11, 12, 13, 14]) {
const start = performance.now()
await bcrypt.hash('test-password', rounds)
console.log(rounds, (performance.now() - start).toFixed(0), 'ms')
}
结果大概是每加 1 翻倍:10 约 60ms、12 约 240ms、14 约 1 秒(具体取决于机器)。
选择标准:单次哈希耗时在 100-250ms 之间。低于 100ms 太弱,高于 250ms 登录接口会成为 DoS 的入口——攻击者用大量登录请求把你的 CPU 打满。
在你自己的服务器上测,不要用开发机的数字。2 核 VPS 和 M4 的差距是数量级。
存储格式
bcrypt 的输出自带算法标识、成本因子和盐:
$2a$12$R9h/cIPz0gi.URNNX3kh2OPST9/PgBkqquzi.Ss7KIUgO2t0jWMUW
│ │ │ │
│ │ └ 22 字符盐 + 31 字符哈希
│ └ 成本因子 12
└ 版本
所以存一个字段就够,不需要单独存盐。校验时直接比对:
const ok = await bcrypt.compare(inputPassword, user.passwordHash)
compare 会从哈希里读出成本和盐,自动用同样的参数计算。
换算法时怎么平滑迁移
不能要求所有用户重设密码。做法是「登录时静默升级」:
async function verify(user, input) {
const ok = await bcrypt.compare(input, user.passwordHash)
if (!ok) return false
// 成本因子偏低,用新参数重新哈希
if (needsRehash(user.passwordHash)) {
const fresh = await bcrypt.hash(input, CURRENT_ROUNDS)
await db.update(users).set({ passwordHash: fresh }).where(eq(users.id, user.id))
}
return true
}
function needsRehash(hash) {
return bcrypt.getRounds(hash) < CURRENT_ROUNDS
}
用户无感,几个月后大部分活跃用户都升到了新参数。不活跃的账号保持旧参数——风险可控,因为他们的密码本来就不常被使用。
三个常见错误
第一,把「慢」当成 bug 去优化。 有人看到登录要 200ms,就把成本降到 4,理由是「用户体验」。这等于把密码存储降级成玩具。
第二,忘了限制尝试次数。 哈希再慢,攻击者也可以无限次尝试。必须配套:按账号 + IP 限流、失败若干次后锁定或触发验证码、记录失败日志。
第三,日志里打印了密码。
console.log('login attempt', { username, password }) // 灾难
这条日志会进 journald、进日志平台、进备份。一次疏忽,全站用户的密码明文躺在磁盘上。在日志库层面加 redact 规则,不要依赖开发者自觉。
还有一个常被忽略的
密码长度上限。bcrypt 只处理前 72 字节,超出的部分被静默截断。用户以为自己的 100 位密码更安全,实际只有前 72 字节生效。
两种处理:限制最大长度(比如 64 字符)并在 UI 上说明;或者先 sha256 再 bcrypt(注意这是「预处理」,不是「替代」)。
收尾清单
- 用 bcrypt / scrypt / argon2,不用 sha256;
- 成本因子在本机实测,单次 100-250ms;
- 只在登录时静默升级参数;
- 失败次数必须限流;
- 日志必须脱敏;
- 明确长度上限。
六条做到,密码存储这一块基本就不会出事了。
你认为这篇文章怎么样?
还没有表情反馈

一起聊聊
一个想法、一点补充,或一句简单的问候。
评论与回复
友善交流,彼此尊重第一句话,留给你。
读完有什么感受?欢迎成为第一个分享的人。