本文目录导读:

在PHP项目中开发安全的找回密码功能,需重点防范账号枚举、令牌泄露、时间差攻击和重放攻击,以下是经过安全实践验证的开发方案:
核心安全原则
- 无差异响应:成功/失败返回相同信息,避免枚举有效账号
- 临时令牌:使用高熵随机数,设置短有效期(通常15-30分钟)
- 一次性使用:令牌使用后立即失效
- 防暴力破解:限制请求频率和尝试次数
安全实现步骤
生成重置令牌
// 生成安全的随机令牌
function generateResetToken() {
return bin2hex(random_bytes(32)); // 64字符十六进制
}
存储令牌(带过期时间)
// 存储到数据库
function storeResetToken($userId, $token) {
$expiresAt = date('Y-m-d H:i:s', strtotime('+30 minutes'));
$hashedToken = hash('sha256', $token); // 存储哈希值
$stmt = $pdo->prepare("INSERT INTO password_resets (user_id, token_hash, expires_at)
VALUES (?, ?, ?)");
$stmt->execute([$userId, $hashedToken, $expiresAt]);
}
发送邮件(关键安全点)
function sendResetEmail($email, $token) {
// 不要提示账号是否存在
// 统一返回"如果账号存在,已发送重置邮件"
$resetLink = "https://yoursite.com/reset-password?token=" . urlencode($token);
// 使用邮件模板,包含用户友好的说明
$subject = "重置您的密码";
$body = "请点击以下链接重置密码(30分钟内有效):\n\n";
$body .= $resetLink;
mail($email, $subject, $body, $headers);
}
验证令牌
function verifyResetToken($token) {
$hashedToken = hash('sha256', $token);
// 查找有效令牌(未使用且未过期)
$stmt = $pdo->prepare("SELECT user_id, expires_at
FROM password_resets
WHERE token_hash = ?
AND used_at IS NULL
AND expires_at > NOW()");
$stmt->execute([$hashedToken]);
$result = $stmt->fetch();
if (!$result) {
return false; // 令牌无效或已过期
}
// 标记为已使用(防止重放)
$stmt = $pdo->prepare("UPDATE password_resets
SET used_at = NOW()
WHERE token_hash = ?");
$stmt->execute([$hashedToken]);
return $result['user_id'];
}
重置密码
function resetPassword($userId, $newPassword) {
// 密码强度验证
if (strlen($newPassword) < 8) {
throw new Exception("密码长度至少8位");
}
// 使用bcrypt哈希
$hashedPassword = password_hash($newPassword, PASSWORD_BCRYPT);
// 更新密码
$stmt = $pdo->prepare("UPDATE users SET password_hash = ? WHERE id = ?");
$stmt->execute([$hashedPassword, $userId]);
// 可选:使其他活跃会话失效
$stmt = $pdo->prepare("DELETE FROM sessions WHERE user_id = ?");
$stmt->execute([$userId]);
}
安全增强措施
速率限制
// 基于IP限制重置请求
function checkRateLimit($ip) {
$window = 3600; // 1小时
$maxAttempts = 5;
$stmt = $pdo->prepare("SELECT COUNT(*) FROM reset_attempts
WHERE ip = ? AND created_at > NOW() - INTERVAL 1 HOUR");
$stmt->execute([$ip]);
$attempts = $stmt->fetchColumn();
if ($attempts >= $maxAttempts) {
throw new Exception("请求过于频繁,请1小时后再试");
}
}
令牌安全存储
-- 数据库表设计
CREATE TABLE password_resets (
id INT AUTO_INCREMENT PRIMARY KEY,
user_id INT NOT NULL,
token_hash VARCHAR(64) NOT NULL, -- SHA256哈希
expires_at DATETIME NOT NULL,
used_at DATETIME DEFAULT NULL,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
INDEX idx_token_hash (token_hash),
INDEX idx_expires (expires_at)
);
清理过期令牌
// 定时任务或每次请求前清理
function cleanExpiredTokens() {
$pdo->exec("DELETE FROM password_resets WHERE expires_at < NOW()");
}
完整流程示例
// 1. 请求重置密码
if ($_POST['action'] == 'request_reset') {
$email = filter_input(INPUT_POST, 'email', FILTER_SANITIZE_EMAIL);
// 速率限制检查
checkRateLimit($_SERVER['REMOTE_ADDR']);
// 查找用户(不要泄露是否存在)
$user = findUserByEmail($email);
if ($user) {
$token = generateResetToken();
storeResetToken($user['id'], $token);
sendResetEmail($email, $token);
}
// 统一响应
echo json_encode(['message' => '如果该邮箱已注册,您将收到重置邮件']);
}
// 2. 验证并重设密码
if ($_POST['action'] == 'reset_password') {
$token = $_POST['token'];
$password = $_POST['password'];
$userId = verifyResetToken($token);
if (!$userId) {
echo json_encode(['error' => '令牌无效或已过期']);
exit;
}
resetPassword($userId, $password);
echo json_encode(['message' => '密码已成功重置']);
}
安全注意事项
- HTTPS强制使用:所有重置流程必须在HTTPS下进行
- CSRF防护:重置表单需包含CSRF令牌
- 日志记录:记录所有重置尝试(但不记录密码)
- 通知用户:密码更改成功后发送通知邮件
- 避免自动登录:重置后不要自动登录,让用户手动登录
采用上述方案,你的PHP找回密码功能将具备企业级的安全防护能力。