Skip to content
On this page

密码安全

密码安全是网络安全的第一道防线。强密码策略和安全的密码处理机制对保护用户账户至关重要。

密码安全基础

为什么密码安全重要

密码是大多数系统的首要认证方式。弱密码、不安全的密码存储或传输都会导致账户被攻破,进而造成数据泄露和安全风险。

常见密码攻击

  • 字典攻击 - 使用常见密码列表尝试登录
  • 暴力破解 - 尝试所有可能的字符组合
  • 彩虹表攻击 - 使用预计算的哈希值查找明文密码
  • 社会工程学 - 通过欺骗获取密码

密码策略

密码强度要求

javascript
// 密码强度验证函数
function validatePasswordStrength(password) {
  const errors = [];

  // 最小长度
  if (password.length < 8) {
    errors.push('Password must be at least 8 characters long');
  }

  // 包含大写字母
  if (!/[A-Z]/.test(password)) {
    errors.push('Password must contain at least one uppercase letter');
  }

  // 包含小写字母
  if (!/[a-z]/.test(password)) {
    errors.push('Password must contain at least one lowercase letter');
  }

  // 包含数字
  if (!/\d/.test(password)) {
    errors.push('Password must contain at least one number');
  }

  // 包含特殊字符
  if (!/[!@#$%^&*(),.?":{}|<>]/.test(password)) {
    errors.push('Password must contain at least one special character');
  }

  // 检查常见弱密码
  const commonPasswords = [
    'password', '123456', 'qwerty', 'abc123', 'password123'
  ];
  if (commonPasswords.some(common => 
    password.toLowerCase().includes(common))) {
    errors.push('Password contains common weak phrases');
  }

  return {
    isValid: errors.length === 0,
    errors
  };
}

// 使用示例
app.post('/register', (req, res) => {
  const { password } = req.body;
  const validation = validatePasswordStrength(password);

  if (!validation.isValid) {
    return res.status(400).json({ 
      error: 'Password validation failed',
      details: validation.errors 
    });
  }

  // 继续注册流程
});

密码历史检查

javascript
// 检查密码历史
async function checkPasswordHistory(userId, newPassword) {
  const user = await User.findById(userId);
  const passwordHistory = user.passwordHistory || [];

  // 检查新密码是否与最近5个密码相同
  for (const oldPasswordHash of passwordHistory.slice(-5)) {
    const isMatch = await bcrypt.compare(newPassword, oldPasswordHash);
    if (isMatch) {
      return {
        valid: false,
        reason: 'Password must be different from recent passwords'
      };
    }
  }

  return { valid: true };
}

// 在密码更新时使用
app.put('/change-password', authenticateJWT, async (req, res) => {
  const { currentPassword, newPassword } = req.body;
  const user = await User.findById(req.user.userId);

  // 验证当前密码
  const isValidCurrent = await bcrypt.compare(currentPassword, user.passwordHash);
  if (!isValidCurrent) {
    return res.status(400).json({ error: 'Current password is incorrect' });
  }

  // 检查密码历史
  const historyCheck = await checkPasswordHistory(user.id, newPassword);
  if (!historyCheck.valid) {
    return res.status(400).json({ error: historyCheck.reason });
  }

  // 验证新密码强度
  const strengthValidation = validatePasswordStrength(newPassword);
  if (!strengthValidation.isValid) {
    return res.status(400).json({ 
      error: 'New password does not meet requirements',
      details: strengthValidation.errors 
    });
  }

  // 更新密码
  const newHash = await bcrypt.hash(newPassword, 12);
  await User.findByIdAndUpdate(user.id, {
    passwordHash: newHash,
    $push: {
      passwordHistory: {
        $each: [user.passwordHash],
        $slice: -6  // 只保留最近6个密码
      }
    },
    lastPasswordChange: new Date()
  });

  res.json({ success: true, message: 'Password changed successfully' });
});

安全的密码存储

使用 bcrypt 进行密码哈希

javascript
const bcrypt = require('bcrypt');

// 密码哈希函数
async function hashPassword(password) {
  // 使用 12 轮盐值生成
  const saltRounds = 12;
  return await bcrypt.hash(password, saltRounds);
}

// 密码验证函数
async function verifyPassword(password, hashedPassword) {
  return await bcrypt.compare(password, hashedPassword);
}

// 在用户注册时使用
app.post('/register', async (req, res) => {
  const { username, email, password } = req.body;

  // 验证密码强度
  const validation = validatePasswordStrength(password);
  if (!validation.isValid) {
    return res.status(400).json({ 
      error: 'Password does not meet requirements',
      details: validation.errors 
    });
  }

  // 检查用户是否已存在
  const existingUser = await User.findOne({ 
    $or: [{ username }, { email }] 
  });
  if (existingUser) {
    return res.status(409).json({ error: 'User already exists' });
  }

  // 哈希密码
  const hashedPassword = await hashPassword(password);

  // 创建用户
  const newUser = await User.create({
    username,
    email,
    passwordHash: hashedPassword,
    createdAt: new Date(),
    lastPasswordChange: new Date()
  });

  res.status(201).json({ 
    success: true, 
    userId: newUser.id,
    message: 'User registered successfully' 
  });
});

使用 Argon2 进行更强的哈希

javascript
const argon2 = require('argon2');

// 使用 Argon2 进行密码哈希
async function hashPasswordArgon2(password) {
  const options = {
    type: argon2.argon2id,  // 推荐的类型
    memoryCost: 2 ** 16,    // 64MB 内存
    timeCost: 3,            // 3 次迭代
    parallelism: 1          // 1 个线程
  };

  return await argon2.hash(password, options);
}

// 验证 Argon2 哈希
async function verifyPasswordArgon2(password, hashedPassword) {
  try {
    return await argon2.verify(hashedPassword, password);
  } catch (error) {
    console.error('Password verification error:', error);
    return false;
  }
}

密码重置机制

安全的密码重置流程

javascript
const crypto = require('crypto');

// 生成密码重置令牌
function generateResetToken() {
  return crypto.randomBytes(32).toString('hex');
}

// 密码重置请求
app.post('/forgot-password', async (req, res) => {
  const { email } = req.body;

  try {
    const user = await User.findOne({ email });
    if (!user) {
      // 为了安全,即使用户不存在也要返回成功消息
      return res.json({ 
        success: true, 
        message: 'If an account exists with this email, a reset link has been sent' 
      });
    }

    // 生成重置令牌
    const resetToken = generateResetToken();
    const resetTokenExpiry = Date.now() + 3600000; // 1小时后过期

    // 保存重置令牌
    await User.findByIdAndUpdate(user._id, {
      resetPasswordToken: resetToken,
      resetPasswordExpires: resetTokenExpiry
    });

    // 发送重置邮件
    await sendPasswordResetEmail(user.email, resetToken);

    res.json({ 
      success: true, 
      message: 'If an account exists with this email, a reset link has been sent' 
    });
  } catch (error) {
    console.error('Forgot password error:', error);
    res.status(500).json({ error: 'An error occurred' });
  }
});

// 重置密码
app.post('/reset-password/:token', async (req, res) => {
  const { token } = req.params;
  const { newPassword } = req.body;

  try {
    // 查找带有有效令牌的用户
    const user = await User.findOne({
      resetPasswordToken: token,
      resetPasswordExpires: { $gt: Date.now() }
    });

    if (!user) {
      return res.status(400).json({ 
        error: 'Password reset token is invalid or has expired' 
      });
    }

    // 验证新密码强度
    const validation = validatePasswordStrength(newPassword);
    if (!validation.isValid) {
      return res.status(400).json({ 
        error: 'New password does not meet requirements',
        details: validation.errors 
      });
    }

    // 哈希新密码
    const hashedPassword = await hashPassword(newPassword);

    // 更新用户密码
    await User.findByIdAndUpdate(user._id, {
      passwordHash: hashedPassword,
      resetPasswordToken: undefined,
      resetPasswordExpires: undefined,
      lastPasswordChange: new Date()
    });

    res.json({ 
      success: true, 
      message: 'Password has been reset successfully' 
    });
  } catch (error) {
    console.error('Reset password error:', error);
    res.status(500).json({ error: 'An error occurred' });
  }
});

速率限制

javascript
const rateLimit = require('express-rate-limit');

// 密码重置请求限制
const resetRequestLimiter = rateLimit({
  windowMs: 15 * 60 * 1000, // 15分钟
  max: 3, // 每15分钟最多3次请求
  message: 'Too many password reset attempts, please try again later',
  standardHeaders: true,
  legacyHeaders: false,
});

// 登录尝试限制
const loginLimiter = rateLimit({
  windowMs: 15 * 60 * 1000, // 15分钟
  max: 5, // 最多5次尝试
  message: 'Too many login attempts, please try again later',
  skipSuccessfulRequests: true
});

// 应用限制
app.post('/forgot-password', resetRequestLimiter, forgotPasswordHandler);
app.post('/login', loginLimiter, loginHandler);

多因素认证 (MFA)

TOTP (基于时间的一次性密码)

javascript
const speakeasy = require('speakeasy');
const QRCode = require('qrcode');

// 为用户设置 TOTP
app.post('/setup-mfa', authenticateJWT, async (req, res) => {
  const user = await User.findById(req.user.userId);

  if (user.mfaEnabled) {
    return res.status(400).json({ error: 'MFA is already enabled' });
  }

  // 生成密钥
  const secret = speakeasy.generateSecret({
    name: `AppName (${user.email})`,
    issuer: 'AppName'
  });

  // 生成二维码
  const qrCodeUrl = await QRCode.toDataURL(secret.otpauth_url);

  // 临时存储密钥(用户验证后保存)
  await User.findByIdAndUpdate(req.user.userId, {
    tempMFASecret: secret.base32
  });

  res.json({
    secret: secret.base32,
    qrCodeUrl,
    otpauthUrl: secret.otpauth_url
  });
});

// 验证 MFA 设置
app.post('/verify-mfa', authenticateJWT, async (req, res) => {
  const { token } = req.body;
  const user = await User.findById(req.user.userId);

  if (!user.tempMFASecret) {
    return res.status(400).json({ error: 'MFA setup not initiated' });
  }

  // 验证令牌
  const verified = speakeasy.totp.verify({
    secret: user.tempMFASecret,
    encoding: 'base32',
    token: token,
    window: 2  // 容忍前后2个时间窗口
  });

  if (!verified) {
    return res.status(400).json({ error: 'Invalid token' });
  }

  // 启用 MFA
  await User.findByIdAndUpdate(req.user.userId, {
    mfaEnabled: true,
    mfaSecret: user.tempMFASecret,
    tempMFASecret: undefined
  });

  res.json({ success: true, message: 'MFA enabled successfully' });
});

// MFA 登录
app.post('/login-mfa', async (req, res) => {
  const { email, password, mfaToken } = req.body;

  const user = await User.findOne({ email });
  if (!user) {
    return res.status(401).json({ error: 'Invalid credentials' });
  }

  // 验证密码
  const isValidPassword = await bcrypt.compare(password, user.passwordHash);
  if (!isValidPassword) {
    return res.status(401).json({ error: 'Invalid credentials' });
  }

  // 如果启用了 MFA,验证令牌
  if (user.mfaEnabled) {
    if (!mfaToken) {
      return res.status(401).json({ error: 'MFA token required' });
    }

    const verified = speakeasy.totp.verify({
      secret: user.mfaSecret,
      encoding: 'base32',
      token: mfaToken,
      window: 2
    });

    if (!verified) {
      return res.status(401).json({ error: 'Invalid MFA token' });
    }
  }

  // 登录成功,生成 JWT
  const jwtToken = generateJWT(user);
  res.json({ 
    success: true, 
    token: jwtToken,
    mfaVerified: true 
  });
});

密码泄露检测

使用 HaveIBeenPwned API

javascript
const axios = require('axios');

// 检查密码是否在泄露数据库中
async function checkPasswordPwned(password) {
  const crypto = require('crypto');
  
  // 使用 K-Anonymity 模型
  const hash = crypto.createHash('sha1').update(password).digest('hex').toUpperCase();
  const prefix = hash.substring(0, 5);
  const suffix = hash.substring(5);
  
  try {
    const response = await axios.get(`https://api.pwnedpasswords.com/range/${prefix}`);
    const hashes = response.data.split('\n');
    
    for (const hashLine of hashes) {
      const [hashSuffix, count] = hashLine.trim().split(':');
      if (hashSuffix === suffix) {
        return {
          pwned: true,
          count: parseInt(count)
        };
      }
    }
    
    return {
      pwned: false
    };
  } catch (error) {
    console.error('Error checking pwned passwords:', error);
    // 如果 API 不可用,可以选择跳过检查
    return { pwned: false };
  }
}

// 在密码验证中使用
async function validatePasswordNotPwned(password) {
  const result = await checkPasswordPwned(password);
  if (result.pwned) {
    throw new Error(`This password has been seen ${result.count} times before and should not be used.`);
  }
  return true;
}

前端密码安全

密码强度实时反馈

html
<!DOCTYPE html>
<html>
<head>
    <title>Password Strength Indicator</title>
    <style>
        .strength-meter {
            height: 5px;
            background: #eee;
            border-radius: 3px;
            margin: 10px 0;
            overflow: hidden;
        }
        
        .strength-bar {
            height: 100%;
            width: 0%;
            transition: width 0.3s ease, background-color 0.3s ease;
        }
        
        .weak { background-color: #ff4757; }
        .medium { background-color: #ffa502; }
        .strong { background-color: #2ed573; }
        
        .requirements {
            font-size: 12px;
            color: #666;
        }
        
        .requirement {
            margin: 2px 0;
        }
        
        .requirement.met {
            color: #2ed573;
        }
        
        .requirement.unmet {
            color: #ff4757;
        }
    </style>
</head>
<body>
    <div>
        <label for="password">Password:</label>
        <input type="password" id="password" placeholder="Enter your password">
        
        <div class="strength-meter">
            <div class="strength-bar" id="strengthBar"></div>
        </div>
        
        <div class="requirements" id="requirementsList">
            <!-- Requirements will be populated here -->
        </div>
    </div>

    <script>
        function calculatePasswordStrength(password) {
            let score = 0;
            const checks = {
                length: password.length >= 8,
                upper: /[A-Z]/.test(password),
                lower: /[a-z]/.test(password),
                number: /\d/.test(password),
                special: /[!@#$%^&*(),.?":{}|<>]/.test(password),
                noCommon: !/(password|123456|qwerty|abc123)/i.test(password)
            };

            // 计算分数
            Object.values(checks).forEach(check => {
                if (check) score++;
            });

            let strength = 'weak';
            let width = 0;
            
            if (score <= 2) {
                strength = 'weak';
                width = 33;
            } else if (score <= 4) {
                strength = 'medium';
                width = 66;
            } else {
                strength = 'strong';
                width = 100;
            }

            return { score, strength, width, checks };
        }

        function updatePasswordStrength() {
            const password = document.getElementById('password').value;
            const strengthBar = document.getElementById('strengthBar');
            const requirementsList = document.getElementById('requirementsList');
            
            if (password.length === 0) {
                strengthBar.style.width = '0%';
                strengthBar.className = 'strength-bar';
                requirementsList.innerHTML = '';
                return;
            }

            const result = calculatePasswordStrength(password);
            
            // 更新强度条
            strengthBar.style.width = result.width + '%';
            strengthBar.className = `strength-bar ${result.strength}`;
            
            // 更新要求列表
            const requirements = [
                { text: 'At least 8 characters', key: 'length' },
                { text: 'Contains uppercase letter', key: 'upper' },
                { text: 'Contains lowercase letter', key: 'lower' },
                { text: 'Contains number', key: 'number' },
                { text: 'Contains special character', key: 'special' },
                { text: 'Not a common password', key: 'noCommon' }
            ];

            requirementsList.innerHTML = requirements.map(req => `
                <div class="requirement ${result.checks[req.key] ? 'met' : 'unmet'}">
                    ${result.checks[req.key] ? '' : ''} ${req.text}
                </div>
            `).join('');
        }

        document.getElementById('password').addEventListener('input', updatePasswordStrength);
    </script>
</body>
</html>

密码安全最佳实践

1. 安全的密码提示

javascript
// 避免提供过于具体的密码提示
function getGenericPasswordHint() {
  return "Use a combination of uppercase letters, lowercase letters, numbers, and special characters";
}

// 而不是
function getSpecificPasswordHint() {
  return "Your password must contain at least 1 uppercase, 2 numbers, and 1 special character (!@#$%)";
}

2. 密码更新提醒

javascript
// 检查密码年龄
function checkPasswordAge(lastChangeDate) {
  const daysSinceChange = (Date.now() - new Date(lastChangeDate).getTime()) / (1000 * 60 * 60 * 24);
  
  if (daysSinceChange > 90) { // 90天
    return {
      needsUpdate: true,
      daysSinceChange: Math.floor(daysSinceChange)
    };
  }
  
  return {
    needsUpdate: false,
    daysSinceChange: Math.floor(daysSinceChange)
  };
}

// 在登录后检查
app.post('/login', async (req, res) => {
  // ... 登录验证逻辑 ...
  
  const passwordAge = checkPasswordAge(user.lastPasswordChange);
  if (passwordAge.needsUpdate) {
    // 要求用户更新密码
    return res.json({
      success: true,
      requiresPasswordUpdate: true,
      message: `Your password is ${passwordAge.daysSinceChange} days old. Please update it.`
    });
  }
  
  // 正常登录流程
});

总结

密码安全是整体安全策略的重要组成部分。关键要点包括:

  1. 实施强密码策略
  2. 使用安全的哈希算法存储密码
  3. 实现安全的密码重置机制
  4. 考虑实施多因素认证
  5. 检测已泄露的密码
  6. 定期提醒用户更新密码
  7. 在前端提供密码强度反馈
  8. 实施适当的速率限制