Appearance
密码安全
密码安全是网络安全的第一道防线。强密码策略和安全的密码处理机制对保护用户账户至关重要。
密码安全基础
为什么密码安全重要
密码是大多数系统的首要认证方式。弱密码、不安全的密码存储或传输都会导致账户被攻破,进而造成数据泄露和安全风险。
常见密码攻击
- 字典攻击 - 使用常见密码列表尝试登录
- 暴力破解 - 尝试所有可能的字符组合
- 彩虹表攻击 - 使用预计算的哈希值查找明文密码
- 社会工程学 - 通过欺骗获取密码
密码策略
密码强度要求
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.`
});
}
// 正常登录流程
});
总结
密码安全是整体安全策略的重要组成部分。关键要点包括:
- 实施强密码策略
- 使用安全的哈希算法存储密码
- 实现安全的密码重置机制
- 考虑实施多因素认证
- 检测已泄露的密码
- 定期提醒用户更新密码
- 在前端提供密码强度反馈
- 实施适当的速率限制