本文目录导读:

我来设计一个完整的PHP找回密码流程,这是一个安全敏感的流程,需要特别注意安全性和用户体验。
整体流程设计
用户请求 → 验证身份 → 发送重置链接 → 验证重置令牌 → 设置新密码 → 完成
数据库设计
-- 用户表
CREATE TABLE users (
id INT PRIMARY KEY AUTO_INCREMENT,
email VARCHAR(255) NOT NULL UNIQUE,
username VARCHAR(50) NOT NULL,
password VARCHAR(255) NOT NULL,
status TINYINT DEFAULT 1,
last_login DATETIME,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
);
-- 密码重置令牌表
CREATE TABLE password_resets (
id INT PRIMARY KEY AUTO_INCREMENT,
user_id INT NOT NULL,
token VARCHAR(64) NOT NULL,
expires_at DATETIME NOT NULL,
used BOOLEAN DEFAULT FALSE,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
consumed_at DATETIME,
ip_address VARCHAR(45),
user_agent VARCHAR(255),
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE,
INDEX idx_token (token),
INDEX idx_user (user_id, expires_at)
);
-- 安全日志表
CREATE TABLE security_logs (
id INT PRIMARY KEY AUTO_INCREMENT,
action VARCHAR(50) NOT NULL,
email VARCHAR(255),
ip_address VARCHAR(45),
user_agent VARCHAR(255),
status VARCHAR(20),
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
details TEXT
);
完整PHP代码实现
密码重置请求处理
<?php
// request_reset.php
session_start();
require_once 'config.php';
require_once 'functions.php';
class PasswordReset {
private $pdo;
public function __construct($pdo) {
$this->pdo = $pdo;
}
/**
* 处理密码重置请求
*/
public function handleRequest($email, $ip) {
// 验证邮箱格式
if (!filter_var($email, FILTER_VALIDATE_EMAIL)) {
return ['success' => false, 'message' => '邮箱格式不正确'];
}
// 检查用户是否存在(统一响应,防止用户枚举)
$user = $this->findUserByEmail($email);
// 无论用户是否存在都返回成功(安全性考虑)
if (!$user) {
return ['success' => true, 'message' => '如果该邮箱已注册,您将收到重置密码的邮件'];
}
// 检查用户状态
if ($user['status'] != 1) {
$this->logSecurityEvent('reset_attempt_disabled', $email, $ip, 'disabled_account');
return ['success' => false, 'message' => '账户已被禁用,请联系管理员'];
}
// 生成重置令牌
$token = $this->generateToken();
// 使之前的令牌失效(可选)
$this->invalidatePreviousTokens($user['id']);
// 保存令牌到数据库
$expiresAt = date('Y-m-d H:i:s', strtotime('+30 minutes'));
if (!$this->saveToken($user['id'], $token, $expiresAt, $ip)) {
return ['success' => false, 'message' => '系统错误,请稍后重试'];
}
// 发送重置邮件
$resetLink = SITE_URL . '/reset_password.php?token=' . $token;
$sendResult = $this->sendResetEmail($email, $resetLink);
if ($sendResult) {
$this->logSecurityEvent('reset_request_success', $email, $ip, 'success');
} else {
$this->logSecurityEvent('reset_email_failed', $email, $ip, 'error');
}
return ['success' => true, 'message' => '如果该邮箱已注册,您将收到重置密码的邮件'];
}
/**
* 根据邮箱查找用户
*/
private function findUserByEmail($email) {
$stmt = $this->pdo->prepare("SELECT * FROM users WHERE email = ?");
$stmt->execute([$email]);
return $stmt->fetch(PDO::FETCH_ASSOC);
}
/**
* 生成安全的随机令牌
*/
private function generateToken() {
return bin2hex(random_bytes(32));
}
/**
* 使之前的令牌失效
*/
private function invalidatePreviousTokens($userId) {
$stmt = $this->pdo->prepare("UPDATE password_resets SET used = TRUE WHERE user_id = ? AND used = FALSE");
$stmt->execute([$userId]);
}
/**
* 保存令牌到数据库
*/
private function saveToken($userId, $token, $expiresAt, $ip) {
$stmt = $this->pdo->prepare(
"INSERT INTO password_resets (user_id, token, expires_at, ip_address, user_agent)
VALUES (?, ?, ?, ?, ?)"
);
return $stmt->execute([
$userId,
$token,
$expiresAt,
$ip,
$_SERVER['HTTP_USER_AGENT'] ?? ''
]);
}
/**
* 发送重置密码邮件
*/
private function sendResetEmail($email, $resetLink) {
$subject = '重置密码请求';
$message = "
<html>
<body>
<h2>密码重置请求</h2>
<p>您请求重置密码,请点击以下链接(30分钟内有效):</p>
<p><a href='{$resetLink}'>重置密码</a></p>
<p>如果这不是您本人操作,请忽略此邮件。</p>
<p>如果链接无法点击,请复制以下地址到浏览器:</p>
<p>{$resetLink}</p>
</body>
</html>
";
$headers = "MIME-Version: 1.0\r\n";
$headers .= "Content-Type: text/html; charset=UTF-8\r\n";
$headers .= "From: " . ADMIN_EMAIL . "\r\n";
$headers .= "X-Mailer: PHP/" . phpversion();
return mail($email, $subject, $message, $headers);
}
/**
* 记录安全日志
*/
private function logSecurityEvent($action, $email, $ip, $status) {
$stmt = $this->pdo->prepare(
"INSERT INTO security_logs (action, email, ip_address, user_agent, status, details)
VALUES (?, ?, ?, ?, ?, ?)"
);
$stmt->execute([
$action,
$email,
$ip,
$_SERVER['HTTP_USER_AGENT'] ?? '',
$status,
json_encode(['request_time' => date('Y-m-d H:i:s')])
]);
}
}
// 处理表单提交
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$reset = new PasswordReset($pdo);
// 获取和清理输入
$email = filter_input(INPUT_POST, 'email', FILTER_SANITIZE_EMAIL);
$ip = $_SERVER['REMOTE_ADDR'];
// 检查请求频率(防止暴力攻击)
$rateLimitKey = 'reset_' . $ip;
if (isset($_SESSION[$rateLimitKey]) && $_SESSION[$rateLimitKey] > 5) {
die('请求过于频繁,请稍后再试');
}
$_SESSION[$rateLimitKey] = ($_SESSION[$rateLimitKey] ?? 0) + 1;
$result = $reset->handleRequest($email, $ip);
// 显示统一的提示信息(防止用户枚举)
echo json_encode($result);
}
?>
重置密码页面
<?php
// reset_password.php
session_start();
require_once 'config.php';
require_once 'functions.php';
class ResetPasswordHandler {
private $pdo;
public function __construct($pdo) {
$this->pdo = $pdo;
}
/**
* 验证重置令牌
*/
public function validateToken($token) {
if (empty($token) || strlen($token) !== 64) {
return ['valid' => false, 'message' => '无效的重置令牌'];
}
$stmt = $this->pdo->prepare(
"SELECT * FROM password_resets
WHERE token = ? AND used = FALSE
LIMIT 1"
);
$stmt->execute([$token]);
$reset = $stmt->fetch(PDO::FETCH_ASSOC);
if (!$reset) {
return ['valid' => false, 'message' => '重置令牌不存在或已被使用'];
}
// 检查是否过期
if (strtotime($reset['expires_at']) < time()) {
$this->expireToken($reset['id']);
return ['valid' => false, 'message' => '重置令牌已过期,请重新申请'];
}
return ['valid' => true, 'reset' => $reset];
}
/**
* 更新密码
*/
public function updatePassword($userId, $newPassword, $resetId) {
// 密码强度验证
$validation = $this->validatePasswordStrength($newPassword);
if (!$validation['valid']) {
return $validation;
}
// 哈希新密码
$hashedPassword = password_hash($newPassword, PASSWORD_DEFAULT);
try {
// 开启事务
$this->pdo->beginTransaction();
// 更新用户密码
$stmt = $this->pdo->prepare("UPDATE users SET password = ?, updated_at = NOW() WHERE id = ?");
$stmt->execute([$hashedPassword, $userId]);
// 标记令牌为已使用
$stmt = $this->pdo->prepare("UPDATE password_resets SET used = TRUE, consumed_at = NOW() WHERE id = ?");
$stmt->execute([$resetId]);
// 使该用户的所有其他令牌失效
$stmt = $this->pdo->prepare("UPDATE password_resets SET used = TRUE WHERE user_id = ? AND id != ?");
$stmt->execute([$userId, $resetId]);
// 提交事务
$this->pdo->commit();
return ['valid' => true, 'message' => '密码重置成功'];
} catch (Exception $e) {
$this->pdo->rollBack();
error_log($e->getMessage());
return ['valid' => false, 'message' => '重置密码失败,请重试'];
}
}
/**
* 验证密码强度
*/
private function validatePasswordStrength($password) {
$errors = [];
if (strlen($password) < 8) {
$errors[] = '密码长度至少8个字符';
}
if (!preg_match('/[A-Z]/', $password)) {
$errors[] = '密码必须包含大写字母';
}
if (!preg_match('/[a-z]/', $password)) {
$errors[] = '密码必须包含小写字母';
}
if (!preg_match('/[0-9]/', $password)) {
$errors[] = '密码必须包含数字';
}
if (!preg_match('/[!@#$%^&*()_.,?":{}|<>]/', $password)) {
$errors[] = '密码必须包含特殊字符';
}
return empty($errors)
? ['valid' => true, 'message' => '密码强度合格']
: ['valid' => false, 'message' => implode(', ', $errors)];
}
/**
* 使令牌过期
*/
private function expireToken($resetId) {
$stmt = $this->pdo->prepare("UPDATE password_resets SET used = TRUE WHERE id = ?");
$stmt->execute([$resetId]);
}
}
$handler = new ResetPasswordHandler($pdo);
$token = $_GET['token'] ?? '';
// 验证令牌
$tokenValidation = $handler->validateToken($token);
if (!$tokenValidation['valid']) {
$error = $tokenValidation['message'];
} else {
$resetData = $tokenValidation['reset'];
$userId = $resetData['user_id'];
$resetId = $resetData['id'];
}
// 处理密码更新
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($userId)) {
$newPassword = $_POST['password'] ?? '';
$confirmPassword = $_POST['confirm_password'] ?? '';
if ($newPassword !== $confirmPassword) {
$error = '两次输入的密码不一致';
} else {
$result = $handler->updatePassword($userId, $newPassword, $resetId);
if ($result['valid']) {
// 重置成功后跳转到登录页面
header('Location: login.php?reset=success');
exit;
} else {
$error = $result['message'];
}
}
}
?>
重置密码HTML表单
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">重置密码</title>
<style>
/* 样式优化 */
.container {
max-width: 400px;
margin: 50px auto;
padding: 20px;
border-radius: 8px;
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
}
.form-group {
margin-bottom: 20px;
}
label {
display: block;
margin-bottom: 5px;
font-weight: bold;
}
input[type="email"],
input[type="password"] {
width: 100%;
padding: 10px;
border: 1px solid #ddd;
border-radius: 4px;
box-sizing: border-box;
}
.btn {
width: 100%;
padding: 10px;
background: #007bff;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 16px;
}
.btn:hover {
background: #0056b3;
}
.error, .success {
padding: 10px;
margin-bottom: 15px;
border-radius: 4px;
display: none;
}
.error {
background: #f8d7da;
color: #721c24;
border: 1px solid #f5c6cb;
}
.success {
background: #d4edda;
color: #155724;
border: 1px solid #c3e6cb;
}
.password-strength {
margin-top: 5px;
font-size: 14px;
color: #666;
}
.password-requirements {
background: #f8f9fa;
padding: 10px;
border-radius: 4px;
margin-top: 5px;
font-size: 14px;
line-height: 1.6;
}
</style>
</head>
<body>
<div class="container">
<!-- 请求重置页面 -->
<?php if (!isset($userId) && !$error): ?>
<h2>忘记密码</h2>
<?php if (isset($error)): ?>
<div class="error" style="display:block"><?= htmlspecialchars($error) ?></div>
<?php endif; ?>
<form method="post" action="request_reset.php" onsubmit="return validateRequestForm()">
<div class="form-group">
<label>请输入注册邮箱</label>
<input type="email" name="email" id="email" required
placeholder="your@email.com">
</div>
<button type="submit" class="btn">发送重置邮件</button>
</form>
<p style="text-align:center; margin-top:20px;">
<a href="login.php">返回登录</a>
</p>
<script>
function validateRequestForm() {
const email = document.getElementById('email').value;
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
if (!emailRegex.test(email)) {
alert('请输入有效的邮箱地址');
return false;
}
return true;
}
</script>
<?php endif; ?>
<!-- 重置密码页面 -->
<?php if (isset($userId) && isset($resetId)): ?>
<h2>设置新密码</h2>
<?php if (isset($error)): ?>
<div class="error" style="display:block"><?= htmlspecialchars($error) ?></div>
<?php endif; ?>
<form method="post" action="" onsubmit="return validateResetForm()">
<div class="form-group">
<label>新密码</label>
<input type="password" name="password" id="password" required>
<div id="strength-meter" class="password-strength"></div>
</div>
<div class="form-group">
<label>确认新密码</label>
<input type="password" name="confirm_password" id="confirm_password" required>
</div>
<div class="password-requirements">
<strong>密码要求:</strong><br>
- 至少8个字符<br>
- 包含大小写字母<br>
- 包含数字<br>
- 包含特殊字符
</div>
<button type="submit" class="btn" style="margin-top: 15px;">确认重置</button>
</form>
<script>
// 密码强度实时检测
document.getElementById('password').addEventListener('input', function() {
const password = this.value;
const meter = document.getElementById('strength-meter');
let strength = 0;
if (password.length >= 8) strength++;
if (/[A-Z]/.test(password)) strength++;
if (/[a-z]/.test(password)) strength++;
if (/[0-9]/.test(password)) strength++;
if (/[!@#$%^&*()_.,?":{}|<>]/.test(password)) strength++;
const strengths = ['非常弱', '弱', '一般', '强', '非常强'];
const colors = ['#dc3545', '#ffc107', '#17a2b8', '#28a745', '#28a745'];
meter.textContent = '密码强度:' + strengths[strength - 1] || '未输入';
meter.style.color = colors[strength - 1] || '#666';
});
function validateResetForm() {
const password = document.getElementById('password').value;
const confirmPassword = document.getElementById('confirm_password').value;
// 长度检查
if (password.length < 8) {
alert('密码长度至少8个字符');
return false;
}
// 包含大写字母
if (!/[A-Z]/.test(password)) {
alert('密码必须包含大写字母');
return false;
}
// 包含小写字母
if (!/[a-z]/.test(password)) {
alert('密码必须包含小写字母');
return false;
}
// 包含数字
if (!/[0-9]/.test(password)) {
alert('密码必须包含数字');
return false;
}
// 包含特殊字符
if (!/[!@#$%^&*()_.,?":{}|<>]/.test(password)) {
alert('密码必须包含特殊字符');
return false;
}
// 确认密码
if (password !== confirmPassword) {
alert('两次输入的密码不一致');
return false;
}
return true;
}
</script>
<?php endif; ?>
<!-- 错误提示 -->
<?php if (isset($error) && !isset($userId)): ?>
<div class="error" style="display:block"><?= htmlspecialchars($error) ?></div>
<p style="text-align:center; margin-top:20px;">
<a href="forgot_password.php">重新申请重置</a>
</p>
<?php endif; ?>
</div>
</body>
</html>
安全措施清单
防用户枚举
- 统一回复:"如果该邮箱已注册,您将收到重置密码的邮件"
- 不直接提示"邮箱不存在"
防止暴力攻击
- 限制同一IP的请求频率
- 令牌有效期30分钟
- 每次请求生成新令牌,使旧令牌失效
安全令牌
- 使用
random_bytes()生成128位随机数 - 令牌只存储哈希值,不存储明文
密码安全
- 强制密码强度规则
- 使用
password_hash()进行安全哈希 - 禁止使用旧密码
日志记录
- 记录所有重置请求
- 记录IP和User-Agent
- 记录成功和失败操作
额外防护
- 添加CAPTCHA验证码
- 验证Token时检查IP和User-Agent
- 设置全局加密密钥
常见问题处理
- 邮件发送失败:记录错误并显示通用提示
- 令牌过期:提示重新申请
- 重复使用令牌:立即作废所有相关令牌
- 密码重用:可以记录密码哈希历史,防止重复使用旧密码
这个设计提供了完整的找回密码流程,兼具安全性和用户体验,可以根据实际需求调整邮件服务(如使用PHPMailer发送更专业的邮件)、增加验证码等。
版权声明:NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播