3、密码安全策略:哈希算法(bcrypt/argon2),加盐与密钥派生
密码存储,是权限系统的第一道防线。
很多人觉得,把密码加密一下存进数据库就完事了。嗯,我以前也这么想。直到有一次做安全审计,看到某个老系统里存的密码还是 MD5 的……说实话,那感觉就像看到自家大门用的是塑料锁。
这一章,我们就把密码存储这件事彻底讲透。从哈希算法到加盐,再到现代的密钥派生函数,一步步把防线筑牢。
3.1 为什么不能直接存密码?
直接存明文密码,等于把用户的家底全亮出来。一旦数据库泄露,所有账号瞬间沦陷。
那用 MD5 或 SHA-1 哈希一下总行了吧?
也不行。你想想看,现在彩虹表满天飞,GPU 跑 MD5 每秒能算几十亿次。我见过一个项目,用的就是 SHA-256 直接哈希密码,结果被撞库撞得干干净净。
3.2 加盐:让每个密码都不一样
加盐,说白了就是在密码后面拼一段随机字符串,然后再做哈希。
为什么要加盐?
- 防止彩虹表预计算攻击
- 让相同密码产生不同哈希值
- 增加破解成本
盐的要求很简单:每个用户、每次密码变更,都用新的随机盐。长度至少 16 字节,用密码学安全的随机数生成器。
3.3 哈希算法对比:bcrypt vs argon2
目前业界主流的密码哈希算法,就两个:bcrypt 和 argon2。
| 特性 | bcrypt | argon2 |
|---|---|---|
| 诞生时间 | 1999 年 | 2015 年(PHC 冠军) |
| 抗 GPU 攻击 | 强(内存需求固定) | 极强(可调内存/并行度) |
| 抗 ASIC 攻击 | 中等 | 强 |
| 参数调节 | cost factor(2^n 轮) | 时间、内存、并行度 |
| 输出格式 | 自带盐和版本 | 自带盐和参数 |
| 推荐场景 | 通用场景,兼容性好 | 高安全场景,新项目首选 |
我个人建议:新项目直接上 argon2。如果项目依赖老框架只能用 bcrypt,那就把 cost 调到能接受的最高值。
3.4 密钥派生:从密码到密钥
有时候,我们不只要验证密码,还要用密码派生出一个加密密钥。比如做端到端加密、本地数据加密。
这时候就需要密钥派生函数(KDF)。
常见的 KDF 有:
- PBKDF2:老牌,但抗 GPU 能力弱
- scrypt:增加了内存消耗,比 PBKDF2 强
- argon2id:推荐,同时抗 GPU 和 ASIC
3.5 实战:用 argon2 存储密码
下面是一个 Node.js 的实现示例。我用的是 argon2 这个 npm 包。
const argon2 = require('argon2');
// 注册:哈希密码
async function hashPassword(password) {
try {
const hash = await argon2.hash(password, {
type: argon2.argon2id, // 推荐类型
memoryCost: 65536, // 64 MB 内存
timeCost: 3, // 3 次迭代
parallelism: 4, // 4 个并行线程
saltLength: 16 // 16 字节随机盐
});
return hash; // 输出包含盐和参数的完整字符串
} catch (err) {
throw new Error('密码哈希失败');
}
}
// 登录:验证密码
async function verifyPassword(hash, password) {
try {
return await argon2.verify(hash, password);
} catch (err) {
return false;
}
}
// 使用示例
const storedHash = await hashPassword('MySecureP@ss123');
const isValid = await verifyPassword(storedHash, 'MySecureP@ss123');
console.log(isValid); // true
注意看,argon2.hash() 返回的字符串里已经包含了盐、参数和哈希值。你不需要自己管理盐,直接存这个字符串就行。
3.6 密码策略的完整流程
一个完整的密码安全策略,不只是哈希算法。它应该包含:
- 密码强度校验:长度、复杂度、常见密码黑名单
- 加盐哈希存储:使用 argon2 或 bcrypt
- 密码历史记录:防止重复使用旧密码
- 登录限速:防止暴力破解
- 密码过期策略:定期更换(可选)
下面这张图,把整个流程串起来了。
3.7 避坑指南
这些年我见过不少密码存储翻车的案例。总结几个最常见的坑:
- 不要自己实现哈希算法:用标准库,别造轮子。我曾经见过有人自己写了个“加强版 MD5”,结果安全性还不如原版。
- 不要截断哈希输出:argon2 的输出长度是固定的,别为了省空间截断。截断等于降低安全性。
- 不要复用盐:每个用户、每次密码变更,都必须生成新盐。全局固定盐等于没加盐。
- 不要忽略参数调优:生产环境要压测,找到安全性和性能的平衡点。我一般从 memoryCost=65536、timeCost=3 开始调。
- 不要忘记密码历史:用户改密码时,要检查新密码是否和最近 5 次历史密码相同。
密码存储这件事,说难不难,说简单也不简单。核心就一句话:用慢速哈希、加随机盐、调好参数。做到这三点,你的系统在密码存储这块就及格了。