密码安全:加盐与哈希

理解密码存储中的哈希和加盐机制

问题

为什么密码不能明文存储?什么是哈希和加盐?如何安全地存储用户密码?

解答

为什么不能明文存储

明文存储密码的风险:

  1. 数据库泄露后,所有用户密码直接暴露
  2. 内部人员可以看到用户密码
  3. 用户往往在多个网站使用相同密码,一处泄露处处危险

哈希(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 等,增加暴力破解成本
  • 前端哈希不能替代后端:前端哈希只是额外保护,后端必须再次哈希