密码安全:加盐与哈希
理解密码存储中的哈希和加盐机制
问题
为什么密码不能明文存储?什么是哈希和加盐?如何安全地存储用户密码?
解答
为什么不能明文存储
明文存储密码的风险:
- 数据库泄露后,所有用户密码直接暴露
- 内部人员可以看到用户密码
- 用户往往在多个网站使用相同密码,一处泄露处处危险
哈希(Hash)
哈希是单向函数,将任意长度的输入转换为固定长度的输出,且不可逆。
const crypto = require('crypto');
// 简单哈希(不安全,仅作演示)
function simpleHash(password) {
return crypto.createHash('sha256').update(password).digest('hex');
}
console.log(simpleHash('123456'));
// 输出: 8d969eef6ecad3c29a3a629280e686cf0c3f5d5a86aff3ca12020c923adc6c92
问题:相同密码产生相同哈希,攻击者可以用彩虹表(预计算的哈希-密码对照表)破解。
加盐(Salt)
盐是随机生成的字符串,与密码拼接后再哈希,使相同密码产生不同的哈希值。
const crypto = require('crypto');
// 生成随机盐
function generateSalt(length = 16) {
return crypto.randomBytes(length).toString('hex');
}
// 加盐哈希
function hashWithSalt(password, salt) {
return crypto.createHash('sha256').update(salt + password).digest('hex');
}
// 创建密码哈希(注册时使用)
function createPassword(password) {
const salt = generateSalt();
const hash = hashWithSalt(password, salt);
// 存储时需要同时保存 salt 和 hash
return { salt, hash };
}
// 验证密码(登录时使用)
function verifyPassword(password, salt, storedHash) {
const hash = hashWithSalt(password, salt);
return hash === storedHash;
}
// 使用示例
const stored = createPassword('myPassword123');
console.log('存储的数据:', stored);
// { salt: 'a1b2c3...', hash: 'x9y8z7...' }
console.log('验证正确密码:', verifyPassword('myPassword123', stored.salt, stored.hash));
// true
console.log('验证错误密码:', verifyPassword('wrongPassword', stored.salt, stored.hash));
// false
更安全的方案:PBKDF2 / bcrypt
实际生产中应使用专门的密码哈希算法,它们内置加盐且计算较慢(防暴力破解)。
const crypto = require('crypto');
// 使用 PBKDF2(Node.js 内置)
function hashPassword(password) {
return new Promise((resolve, reject) => {
const salt = crypto.randomBytes(16).toString('hex');
// iterations 越高越安全,但也越慢
crypto.pbkdf2(password, salt, 100000, 64, 'sha512', (err, derivedKey) => {
if (err) reject(err);
resolve({
salt,
hash: derivedKey.toString('hex'),
iterations: 100000
});
});
});
}
function verifyPasswordPBKDF2(password, salt, storedHash, iterations) {
return new Promise((resolve, reject) => {
crypto.pbkdf2(password, salt, iterations, 64, 'sha512', (err, derivedKey) => {
if (err) reject(err);
resolve(derivedKey.toString('hex') === storedHash);
});
});
}
// 使用示例
async function demo() {
const result = await hashPassword('myPassword123');
console.log('PBKDF2 结果:', result);
const isValid = await verifyPasswordPBKDF2(
'myPassword123',
result.salt,
result.hash,
result.iterations
);
console.log('验证结果:', isValid);
}
demo();
前端 Web Crypto API
// 浏览器环境使用 Web Crypto API
async function hashPasswordBrowser(password) {
const encoder = new TextEncoder();
// 生成随机盐
const salt = crypto.getRandomValues(new Uint8Array(16));
// 导入密码作为密钥材料
const keyMaterial = await crypto.subtle.importKey(
'raw',
encoder.encode(password),
'PBKDF2',
false,
['deriveBits']
);
// 派生密钥
const derivedBits = await crypto.subtle.deriveBits(
{
name: 'PBKDF2',
salt: salt,
iterations: 100000,
hash: 'SHA-256'
},
keyMaterial,
256
);
// 转换为十六进制字符串
const hashArray = Array.from(new Uint8Array(derivedBits));
const hashHex = hashArray.map(b => b.toString(16).padStart(2, '0')).join('');
const saltHex = Array.from(salt).map(b => b.toString(16).padStart(2, '0')).join('');
return { salt: saltHex, hash: hashHex };
}
关键点
- 哈希是单向的:无法从哈希值反推原始密码
- 盐必须随机且唯一:每个用户的盐都不同,防止彩虹表攻击
- 盐需要和哈希一起存储:验证时需要用相同的盐
- 使用慢哈希算法:PBKDF2、bcrypt、scrypt 等,增加暴力破解成本
- 前端哈希不能替代后端:前端哈希只是额外保护,后端必须再次哈希
目录