用户注册登录系统
项目概述
用户注册登录系统是Web应用的基础组件,几乎所有的Web应用都需要用户认证功能。这个项目将综合运用前面所学的PHP知识,包括数据库操作、表单处理、会话管理、安全防护等。
项目目标
完成本项目后,你将能够:
- 掌握完整的用户认证系统开发
- 理解MVC架构的实际应用
- 实现安全的密码存储和验证
- 处理表单验证和错误处理
- 实现会话管理和权限控制
- 学会邮件发送功能
- 掌握安全编码实践
功能特性
核心功能
- 用户注册(邮箱验证)
- 用户登录(记住登录状态)
- 密码重置(通过邮件)
- 用户资料管理
- 登出功能
安全特性
- 密码哈希存储
- SQL注入防护
- XSS攻击防护
- CSRF防护
- 登录失败锁定
- 会话安全
系统架构
目录结构
auth-system/
├── public/ # Web根目录
│ ├── index.php # 入口文件
│ ├── css/
│ │ └── style.css # 样式文件
│ ├── js/
│ │ └── script.js # JavaScript文件
│ └── uploads/ # 上传文件
├── src/ # 源代码
│ ├── config/
│ │ ├── database.php # 数据库配置
│ │ ├── app.php # 应用配置
│ │ └── constants.php # 常量定义
│ ├── controllers/
│ │ ├── BaseController.php
│ │ ├── AuthController.php
│ │ └── UserController.php
│ ├── models/
│ │ ├── Database.php # 数据库基类
│ │ ├── User.php # 用户模型
│ │ ├── Auth.php # 认证模型
│ │ └── PasswordReset.php # 密码重置模型
│ ├── views/
│ │ ├── layouts/
│ │ │ ├── header.php
│ │ │ └── footer.php
│ │ ├── auth/
│ │ │ ├── login.php
│ │ │ ├── register.php
│ │ │ └── reset.php
│ │ └── user/
│ │ ├── dashboard.php
│ │ ├── profile.php
│ │ └── edit.php
│ └── helpers/
│ ├── Validator.php # 验证助手
│ ├── Session.php # 会话助手
│ ├── Mailer.php # 邮件助手
│ └── Security.php # 安全助手
├── database/
│ ├── create_tables.sql # 建表SQL
│ └── seed_data.sql # 测试数据
├── logs/ # 日志目录
├── README.md
└── .htaccess # Apache配置
数据库设计
1. 创建数据库表
-- database/create_tables.sql
-- 用户表
CREATE TABLE users (
id INT PRIMARY KEY AUTO_INCREMENT,
username VARCHAR(50) UNIQUE NOT NULL,
email VARCHAR(100) UNIQUE NOT NULL,
password_hash VARCHAR(255) NOT NULL,
email_verified BOOLEAN DEFAULT FALSE,
email_verification_token VARCHAR(255),
status ENUM('active', 'inactive', 'suspended') DEFAULT 'inactive',
role ENUM('user', 'admin') DEFAULT 'user',
failed_attempts INT DEFAULT 0,
locked_until TIMESTAMP NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
last_login_at TIMESTAMP NULL
);
-- 用户资料表
CREATE TABLE user_profiles (
user_id INT PRIMARY KEY,
first_name VARCHAR(50),
last_name VARCHAR(50),
phone VARCHAR(20),
avatar VARCHAR(255),
bio TEXT,
birth_date DATE,
gender ENUM('male', 'female', 'other'),
website VARCHAR(255),
location VARCHAR(100),
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
);
-- 密码重置表
CREATE TABLE password_resets (
id INT PRIMARY KEY AUTO_INCREMENT,
user_id INT NOT NULL,
token VARCHAR(255) NOT NULL,
expires_at TIMESTAMP NOT NULL,
used BOOLEAN DEFAULT FALSE,
ip_address VARCHAR(45),
user_agent TEXT,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
);
-- 登录日志表
CREATE TABLE login_logs (
id INT PRIMARY KEY AUTO_INCREMENT,
user_id INT,
ip_address VARCHAR(45) NOT NULL,
user_agent TEXT,
success BOOLEAN NOT NULL,
failure_reason VARCHAR(255),
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE SET NULL
);
-- 会话表
CREATE TABLE user_sessions (
id INT PRIMARY KEY AUTO_INCREMENT,
session_id VARCHAR(255) UNIQUE NOT NULL,
user_id INT NOT NULL,
ip_address VARCHAR(45) NOT NULL,
user_agent TEXT,
expires_at TIMESTAMP NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
);
-- 创建索引
CREATE INDEX idx_users_email ON users(email);
CREATE INDEX idx_users_username ON users(username);
CREATE INDEX idx_password_resets_token ON password_resets(token);
CREATE INDEX idx_password_resets_expires ON password_resets(expires_at);
CREATE INDEX idx_login_logs_user_id ON login_logs(user_id);
CREATE INDEX idx_login_logs_created_at ON login_logs(created_at);
CREATE INDEX idx_user_sessions_session_id ON user_sessions(session_id);
CREATE INDEX idx_user_sessions_expires_at ON user_sessions(expires_at);
核心代码实现
1. 配置文件
数据库配置 (src/config/database.php)
<?php
class Database {
private static $instance = null;
private $pdo;
private $host = 'localhost';
private $dbname = 'auth_system';
private $username = 'root';
private $password = '';
private $charset = 'utf8mb4';
private function __construct() {
$dsn = "mysql:host={$this->host};dbname={$this->dbname};charset={$this->charset}";
$options = [
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
PDO::ATTR_EMULATE_PREPARES => false,
PDO::ATTR_PERSISTENT => true
];
try {
$this->pdo = new PDO($dsn, $this->username, $this->password, $options);
} catch (PDOException $e) {
die("数据库连接失败: " . $e->getMessage());
}
}
public static function getInstance() {
if (self::$instance === null) {
self::$instance = new self();
}
return self::$instance;
}
public function getConnection() {
return $this->pdo;
}
public function query($sql, $params = []) {
try {
$stmt = $this->pdo->prepare($sql);
$stmt->execute($params);
return $stmt;
} catch (PDOException $e) {
error_log("查询错误: " . $e->getMessage());
throw new Exception("数据库操作失败");
}
}
public function beginTransaction() {
return $this->pdo->beginTransaction();
}
public function commit() {
return $this->pdo->commit();
}
public function rollBack() {
return $this->pdo->rollBack();
}
public function lastInsertId() {
return $this->pdo->lastInsertId();
}
}
2. 基础控制器 (src/controllers/BaseController.php)
<?php
class BaseController {
protected $db;
public function __construct() {
$this->db = Database::getInstance()->getConnection();
}
protected function render($view, $data = []) {
extract($data);
// 包含头部
include __DIR__ . '/../views/layouts/header.php';
// 包含视图
include __DIR__ . '/../views/' . $view;
// 包含尾部
include __DIR__ . '/../views/layouts/footer.php';
}
protected function json($data, $statusCode = 200) {
http_response_code($statusCode);
header('Content-Type: application/json');
echo json_encode($data);
exit;
}
protected function redirect($url) {
header("Location: {$url}");
exit;
}
protected function isPost() {
return $_SERVER['REQUEST_METHOD'] === 'POST';
}
protected function getPost($key = null, $default = null) {
if ($key === null) {
return $_POST;
}
return $_POST[$key] ?? $default;
}
protected function getGet($key = null, $default = null) {
if ($key === null) {
return $_GET;
}
return $_GET[$key] ?? $default;
}
protected function getSession($key = null, $default = null) {
if (session_status() === PHP_SESSION_NONE) {
session_start();
}
if ($key === null) {
return $_SESSION;
}
return $_SESSION[$key] ?? $default;
}
protected function setSession($key, $value) {
if (session_status() === PHP_SESSION_NONE) {
session_start();
}
$_SESSION[$key] = $value;
}
protected function unsetSession($key) {
if (session_status() === PHP_SESSION_NONE) {
session_start();
}
unset($_SESSION[$key]);
}
}
3. 用户模型 (src/models/User.php)
<?php
require_once __DIR__ . '/../config/database.php';
class User extends BaseController {
public function create($userData) {
// 验证邮箱是否已存在
if ($this->findByEmail($userData['email'])) {
throw new Exception("邮箱已被注册");
}
// 验证用户名是否已存在
if ($this->findByUsername($userData['username'])) {
throw new Exception("用户名已被使用");
}
// 密码哈希
$passwordHash = password_hash($userData['password'], PASSWORD_DEFAULT);
$emailToken = bin2hex(random_bytes(32));
$sql = "INSERT INTO users (username, email, password_hash, email_verification_token, status, created_at)
VALUES (?, ?, ?, ?, 'inactive', NOW())";
$params = [
$userData['username'],
$userData['email'],
$passwordHash,
$emailToken
];
$stmt = $this->db->prepare($sql);
$stmt->execute($params);
$userId = $this->db->lastInsertId();
// 创建用户资料记录
$this->createProfile($userId);
return $userId;
}
public function authenticate($email, $password, $ip = null) {
$user = $this->findByEmail($email);
if (!$user) {
// 记录失败日志
$this->recordLoginAttempt(null, $ip, false, '用户不存在');
return false;
}
// 检查账户状态
if ($user['status'] !== 'active') {
$this->recordLoginAttempt($user['id'], $ip, false, '账户未激活');
return false;
}
// 检查账户是否被锁定
if ($user['locked_until'] && strtotime($user['locked_until']) > time()) {
$this->recordLoginAttempt($user['id'], $ip, false, '账户被锁定');
return false;
}
// 验证密码
if (!password_verify($password, $user['password_hash'])) {
$this->handleFailedLogin($user, $ip);
return false;
}
// 登录成功
$this->handleSuccessfulLogin($user, $ip);
return $user;
}
public function findById($id) {
$sql = "SELECT u.*, p.first_name, p.last_name, p.avatar, p.bio
FROM users u
LEFT JOIN user_profiles p ON u.id = p.user_id
WHERE u.id = ? AND u.status = 'active'";
$stmt = $this->db->prepare($sql);
$stmt->execute([$id]);
return $stmt->fetch();
}
public function findByEmail($email) {
$sql = "SELECT * FROM users WHERE email = ?";
$stmt = $this->db->prepare($sql);
$stmt->execute([$email]);
return $stmt->fetch();
}
public function findByUsername($username) {
$sql = "SELECT * FROM users WHERE username = ?";
$stmt = $this->db->prepare($sql);
$stmt->execute([$username]);
return $stmt->fetch();
}
public function updateProfile($userId, $profileData) {
$sql = "UPDATE user_profiles
SET first_name = ?, last_name = ?, phone = ?, bio = ?,
gender = ?, birth_date = ?, website = ?, location = ?
WHERE user_id = ?";
$params = [
$profileData['first_name'],
$profileData['last_name'],
$profileData['phone'],
$profileData['bio'],
$profileData['gender'],
$profileData['birth_date'],
$profileData['website'],
$profileData['location'],
$userId
];
$stmt = $this->db->prepare($sql);
return $stmt->execute($params);
}
public function updatePassword($userId, $newPassword) {
$passwordHash = password_hash($newPassword, PASSWORD_DEFAULT);
$sql = "UPDATE users SET password_hash = ?, updated_at = NOW() WHERE id = ?";
$stmt = $this->db->prepare($sql);
return $stmt->execute([$passwordHash, $userId]);
}
public function verifyEmail($token) {
$sql = "SELECT id FROM users WHERE email_verification_token = ? AND status = 'inactive'";
$stmt = $this->db->prepare($sql);
$stmt->execute([$token]);
$user = $stmt->fetch();
if ($user) {
$sql = "UPDATE users SET status = 'active', email_verified = TRUE,
email_verification_token = NULL, updated_at = NOW()
WHERE id = ?";
$stmt = $this->db->prepare($sql);
$stmt->execute([$user['id']]);
return true;
}
return false;
}
private function createProfile($userId) {
$sql = "INSERT INTO user_profiles (user_id, created_at) VALUES (?, NOW())";
$stmt = $this->db->prepare($sql);
$stmt->execute([$userId]);
}
private function handleFailedLogin($user, $ip) {
// 增加失败次数
$failedAttempts = $user['failed_attempts'] + 1;
$sql = "UPDATE users SET failed_attempts = ?, updated_at = NOW() WHERE id = ?";
$stmt = $this->db->prepare($sql);
$stmt->execute([$failedAttempts, $user['id']]);
// 如果失败次数达到5次,锁定账户30分钟
if ($failedAttempts >= 5) {
$lockedUntil = date('Y-m-d H:i:s', time() + 1800);
$sql = "UPDATE users SET locked_until = ? WHERE id = ?";
$stmt = $this->db->prepare($sql);
$stmt->execute([$lockedUntil, $user['id']]);
$this->recordLoginAttempt($user['id'], $ip, false, '账户被锁定');
} else {
$this->recordLoginAttempt($user['id'], $ip, false, '密码错误');
}
}
private function handleSuccessfulLogin($user, $ip) {
// 重置失败次数
$sql = "UPDATE users SET failed_attempts = 0, locked_until = NULL,
last_login_at = NOW(), updated_at = NOW()
WHERE id = ?";
$stmt = $this->db->prepare($sql);
$stmt->execute([$user['id']]);
// 记录成功登录
$this->recordLoginAttempt($user['id'], $ip, true);
}
private function recordLoginAttempt($userId, $ip, $success, $reason = null) {
$sql = "INSERT INTO login_logs (user_id, ip_address, user_agent, success, failure_reason, created_at)
VALUES (?, ?, ?, ?, ?, NOW())";
$params = [
$userId,
$ip ?? $_SERVER['REMOTE_ADDR'],
$_SERVER['HTTP_USER_AGENT'] ?? null,
$success,
$reason
];
$stmt = $this->db->prepare($sql);
$stmt->execute($params);
}
}
4. 认证控制器 (src/controllers/AuthController.php)
<?php
require_once 'BaseController.php';
require_once __DIR__ . '/../models/User.php';
require_once __DIR__ . '/../models/PasswordReset.php';
require_once __DIR__ . '/../helpers/Validator.php';
require_once __DIR__ . '/../helpers/Mailer.php';
require_once __DIR__ . '/../helpers/Security.php';
class AuthController extends BaseController {
private $user;
private $passwordReset;
public function __construct() {
parent::__construct();
$this->user = new User();
$this->passwordReset = new PasswordReset();
}
// 显示注册页面
public function register() {
if ($this->getSession('user_id')) {
$this->redirect('dashboard.php');
}
$this->render('auth/register.php');
}
// 处理注册请求
public function doRegister() {
if (!$this->isPost()) {
$this->json(['success' => false, 'message' => '无效的请求方法']);
}
$data = [
'username' => trim($this->getPost('username')),
'email' => trim($this->getPost('email')),
'password' => $this->getPost('password'),
'confirm_password' => $this->getPost('confirm_password')
];
// 验证数据
$validator = new Validator();
$errors = $validator->validateRegistration($data);
if (!empty($errors)) {
$this->json(['success' => false, 'errors' => $errors]);
}
try {
// CSRF保护
if (!Security::validateCsrfToken($this->getPost('csrf_token'))) {
throw new Exception('请求无效,请刷新页面重试');
}
// 创建用户
$userId = $this->user->create($data);
// 发送验证邮件
$user = $this->user->findById($userId);
$mailer = new Mailer();
$mailer->sendVerificationEmail($user['email'], $user['email_verification_token']);
$this->json([
'success' => true,
'message' => '注册成功!请查收邮件验证账户。'
]);
} catch (Exception $e) {
error_log("注册错误: " . $e->getMessage());
$this->json(['success' => false, 'message' => $e->getMessage()]);
}
}
// 显示登录页面
public function login() {
if ($this->getSession('user_id')) {
$this->redirect('dashboard.php');
}
$this->render('auth/login.php');
}
// 处理登录请求
public function doLogin() {
if (!$this->isPost()) {
$this->json(['success' => false, 'message' => '无效的请求方法']);
}
$email = trim($this->getPost('email'));
$password = $this->getPost('password');
$remember = $this->getPost('remember') === 'true';
try {
// 验证CSRF令牌
if (!Security::validateCsrfToken($this->getPost('csrf_token'))) {
throw new Exception('请求无效,请刷新页面重试');
}
// 验证用户
$user = $this->user->authenticate($email, $password);
if (!$user) {
throw new Exception('邮箱或密码错误');
}
// 创建会话
$this->createSession($user, $remember);
$this->json([
'success' => true,
'message' => '登录成功',
'redirect' => 'dashboard.php'
]);
} catch (Exception $e) {
$this->json(['success' => false, 'message' => $e->getMessage()]);
}
}
// 邮箱验证
public function verifyEmail() {
$token = $this->getGet('token');
if (!$token) {
$this->setSession('error', '验证链接无效');
$this->redirect('login.php');
}
if ($this->user->verifyEmail($token)) {
$this->setSession('success', '邮箱验证成功,请登录');
} else {
$this->setSession('error', '验证链接无效或已过期');
}
$this->redirect('login.php');
}
// 显示忘记密码页面
public function forgotPassword() {
$this->render('auth/reset.php', ['step' => 1]);
}
// 处理忘记密码请求
public function doForgotPassword() {
if (!$this->isPost()) {
$this->json(['success' => false, 'message' => '无效的请求方法']);
}
$email = trim($this->getPost('email'));
// 验证邮箱格式
if (!filter_var($email, FILTER_VALIDATE_EMAIL)) {
$this->json(['success' => false, 'message' => '邮箱格式错误']);
}
$user = $this->user->findByEmail($email);
// 无论用户是否存在都显示成功信息(防止邮箱枚举攻击)
if ($user) {
try {
$token = $this->passwordReset->createToken($user['id']);
$mailer = new Mailer();
$mailer->sendPasswordResetEmail($email, $token);
} catch (Exception $e) {
error_log("发送重置邮件错误: " . $e->getMessage());
}
}
$this->json([
'success' => true,
'message' => '如果该邮箱已注册,您将收到密码重置链接'
]);
}
// 显示重置密码页面
public function resetPassword() {
$token = $this->getGet('token');
$step = $this->getGet('step', 2);
if (!$token) {
$this->setSession('error', '重置链接无效');
$this->redirect('login.php');
}
// 验证令牌
$resetData = $this->passwordReset->validateToken($token);
if (!$resetData) {
$this->setSession('error', '重置链接无效或已过期');
$this->redirect('login.php');
}
$this->render('auth/reset.php', [
'step' => $step,
'token' => $token,
'email' => $resetData['email']
]);
}
// 处理重置密码
public function doResetPassword() {
if (!$this->isPost()) {
$this->json(['success' => false, 'message' => '无效的请求方法']);
}
$token = $this->getPost('token');
$password = $this->getPost('password');
$confirmPassword = $this->getPost('confirm_password');
// 验证密码
if (strlen($password) < 6) {
$this->json(['success' => false, 'message' => '密码至少6个字符']);
}
if ($password !== $confirmPassword) {
$this->json(['success' => false, 'message' => '两次输入的密码不一致']);
}
// 验证令牌
$resetData = $this->passwordReset->validateToken($token);
if (!$resetData) {
$this->json(['success' => false, 'message' => '重置链接无效或已过期']);
}
try {
// 更新密码
$this->user->updatePassword($resetData['user_id'], $password);
// 标记令牌为已使用
$this->passwordReset->markAsUsed($token);
$this->json(['success' => true, 'message' => '密码重置成功']);
} catch (Exception $e) {
$this->json(['success' => false, 'message' => '密码重置失败']);
}
}
// 登出
public function logout() {
// 销毁会话
if (session_status() === PHP_SESSION_ACTIVE) {
session_destroy();
}
// 清除Cookie
if (isset($_COOKIE['remember_token'])) {
setcookie('remember_token', '', time() - 3600, '/');
unset($_COOKIE['remember_token']);
}
$this->redirect('login.php');
}
// 创建会话
private function createSession($user, $remember = false) {
// 生成会话ID
$sessionId = bin2hex(random_bytes(32));
$expires = time() + ($remember ? 86400 * 30 : 3600); // 30天或1小时
// 存储会话
$sql = "INSERT INTO user_sessions (session_id, user_id, ip_address, user_agent, expires_at)
VALUES (?, ?, ?, ?, ?)";
$stmt = $this->db->prepare($sql);
$stmt->execute([
$sessionId,
$user['id'],
$_SERVER['REMOTE_ADDR'],
$_SERVER['HTTP_USER_AGENT'],
date('Y-m-d H:i:s', $expires)
]);
// 设置Session
$this->setSession('user_id', $user['id']);
$this->setSession('username', $user['username']);
$this->setSession('email', $user['email']);
$this->setSession('role', $user['role']);
$this->setSession('session_id', $sessionId);
// 设置记住登录Cookie
if ($remember) {
$token = bin2hex(random_bytes(32));
$this->db->prepare("UPDATE users SET remember_token = ? WHERE id = ?")
->execute([$token, $user['id']]);
setcookie('remember_token', $token, $expires, '/', '', true, true);
}
}
}
5. 视图文件示例
注册页面 (src/views/auth/register.php)
<div class="container">
<div class="row justify-content-center">
<div class="col-md-6">
<div class="card">
<div class="card-header">
<h4 class="text-center">用户注册</h4>
</div>
<div class="card-body">
<?php if (isset($_SESSION['error'])): ?>
<div class="alert alert-danger">
<?php
echo $_SESSION['error'];
unset($_SESSION['error']);
?>
</div>
<?php endif; ?>
<?php if (isset($_SESSION['success'])): ?>
<div class="alert alert-success">
<?php
echo $_SESSION['success'];
unset($_SESSION['success']);
?>
</div>
<?php endif; ?>
<form id="registerForm">
<input type="hidden" name="csrf_token" value="<?php echo Security::generateCsrfToken(); ?>">
<div class="form-group mb-3">
<label for="username">用户名</label>
<input type="text" class="form-control" id="username" name="username" required>
<div class="invalid-feedback"></div>
</div>
<div class="form-group mb-3">
<label for="email">邮箱</label>
<input type="email" class="form-control" id="email" name="email" required>
<div class="invalid-feedback"></div>
</div>
<div class="form-group mb-3">
<label for="password">密码</label>
<input type="password" class="form-control" id="password" name="password" required>
<div class="invalid-feedback"></div>
<small class="text-muted">密码至少8个字符,包含字母和数字</small>
</div>
<div class="form-group mb-3">
<label for="confirm_password">确认密码</label>
<input type="password" class="form-control" id="confirm_password" name="confirm_password" required>
<div class="invalid-feedback"></div>
</div>
<div class="d-grid">
<button type="submit" class="btn btn-primary">注册</button>
</div>
</form>
<hr>
<div class="text-center">
<p>已有账户? <a href="login.php">立即登录</a></p>
</div>
</div>
</div>
</div>
</div>
</div>
<script>
document.getElementById('registerForm').addEventListener('submit', function(e) {
e.preventDefault();
const formData = new FormData(this);
const submitBtn = this.querySelector('button[type="submit"]');
const originalText = submitBtn.textContent;
submitBtn.disabled = true;
submitBtn.textContent = '注册中...';
fetch('api/register.php', {
method: 'POST',
body: formData
})
.then(response => response.json())
.then(data => {
if (data.success) {
alert(data.message);
window.location.href = 'login.php';
} else {
// 显示错误信息
if (data.errors) {
Object.keys(data.errors).forEach(field => {
const input = document.querySelector(`[name="${field}"]`);
const feedback = input.nextElementSibling;
input.classList.add('is-invalid');
feedback.textContent = data.errors[field];
});
} else {
alert(data.message);
}
}
})
.catch(error => {
console.error('Error:', error);
alert('注册失败,请稍后重试');
})
.finally(() => {
submitBtn.disabled = false;
submitBtn.textContent = originalText;
});
});
</script>
6. 安全助手 (src/helpers/Security.php)
<?php
class Security {
// 生成CSRF令牌
public static function generateCsrfToken() {
if (!isset($_SESSION['csrf_token'])) {
$_SESSION['csrf_token'] = bin2hex(random_bytes(32));
}
return $_SESSION['csrf_token'];
}
// 验证CSRF令牌
public static function validateCsrfToken($token) {
if (!isset($_SESSION['csrf_token'])) {
return false;
}
return hash_equals($_SESSION['csrf_token'], $token);
}
// XSS过滤
public static function escape($string) {
return htmlspecialchars($string, ENT_QUOTES, 'UTF-8');
}
// SQL注入过滤(虽然使用预处理语句,但额外的保护总是好的)
public static function escapeSql($value) {
// 转义特殊字符
$search = array("\\", "\x00", "\n", "\r", "'", '"', "\x1a");
$replace = array("\\\\", "\\0", "\\n", "\\r", "\\'", '\\"', "\\Z");
return str_replace($search, $replace, $value);
}
// 清理输入
public static function cleanInput($input) {
if (is_array($input)) {
return array_map([self::class, 'cleanInput'], $input);
}
$input = trim($input);
// 移除可能的恶意代码
$input = preg_replace('/<script\b[^<]*(?:(?!<\/script>)<[^<]*)*<\/script>/mi', '', $input);
$input = preg_replace('/<iframe\b[^<]*(?:(?!<\/iframe>)<[^<]*)*<\/iframe>/mi', '', $input);
return $input;
}
// 生成安全的随机字符串
public static function generateRandomString($length = 32) {
return bin2hex(random_bytes($length / 2));
}
// 验证文件上传
public static function validateFileUpload($file, $allowedTypes = [], $maxSize = 5242880) { // 5MB
$errors = [];
// 检查文件是否存在
if (!isset($file) || $file['error'] === UPLOAD_ERR_NO_FILE) {
$errors[] = '没有选择文件';
return $errors;
}
// 检查上传错误
if ($file['error'] !== UPLOAD_ERR_OK) {
$errors[] = '文件上传失败';
return $errors;
}
// 检查文件大小
if ($file['size'] > $maxSize) {
$errors[] = '文件大小超过限制';
}
// 检查文件类型
if (!empty($allowedTypes)) {
$finfo = finfo_open(FILEINFO_MIME_TYPE);
$mimeType = finfo_file($finfo, $file['tmp_name']);
finfo_close($finfo);
if (!in_array($mimeType, $allowedTypes)) {
$errors[] = '文件类型不被允许';
}
}
return $errors;
}
// 安全的文件名生成
public static function generateSafeFilename($originalName) {
$extension = pathinfo($originalName, PATHINFO_EXTENSION);
$filename = pathinfo($originalName, PATHINFO_FILENAME);
// 清理文件名
$filename = preg_replace('/[^A-Za-z0-9_\-]/', '', $filename);
$filename = substr($filename, 0, 50); // 限制长度
// 生成唯一文件名
$uniqueName = $filename . '_' . date('YmdHis') . '_' . uniqid();
return $uniqueName . ($extension ? '.' . $extension : '');
}
}
7. 邮件助手 (src/helpers/Mailer.php)
<?php
class Mailer {
private $fromEmail;
private $fromName;
private $smtpHost;
private $smtpPort;
private $smtpUser;
private $smtpPass;
public function __construct() {
$this->fromEmail = 'noreply@yourdomain.com';
$this->fromName = '您的网站';
// SMTP配置(使用PHPMailer)
$this->smtpHost = 'smtp.yourdomain.com';
$this->smtpPort = 587;
$this->smtpUser = 'your-email@yourdomain.com';
$this->smtpPass = 'your-password';
}
// 发送验证邮件
public function sendVerificationEmail($toEmail, $token) {
$subject = '验证您的邮箱地址';
$verificationUrl = "http://" . $_SERVER['HTTP_HOST'] . "/verify-email.php?token={$token}";
$message = $this->getEmailTemplate('verification', [
'name' => '',
'verification_url' => $verificationUrl,
'token' => $token
]);
return $this->send($toEmail, $subject, $message);
}
// 发送密码重置邮件
public function sendPasswordResetEmail($toEmail, $token) {
$subject = '重置您的密码';
$resetUrl = "http://" . $_SERVER['HTTP_HOST'] . "/reset-password.php?token={$token}&step=2";
$message = $this->getEmailTemplate('password_reset', [
'reset_url' => $resetUrl,
'token' => $token
]);
return $this->send($toEmail, $subject, $message);
}
// 通用邮件发送方法
private function send($to, $subject, $message) {
// 邮件头
$headers = "MIME-Version: 1.0" . "\r\n";
$headers .= "From: {$this->fromName} <{$this->fromEmail}>" . "\r\n";
$headers .= "Reply-To: {$this->fromEmail}" . "\r\n";
$headers .= "Content-Type: text/html; charset=UTF-8" . "\r\n";
// 使用PHPMailer的示例(需要安装PHPMailer)
// 这里提供基本的mail()函数实现
$success = mail($to, $subject, $message, $headers);
if (!$success) {
error_log("邮件发送失败: 收件人={$to}, 主题={$subject}");
}
return $success;
}
// 获取邮件模板
private function getEmailTemplate($type, $data = []) {
$templates = [
'verification' => '
<html>
<head>
<meta charset="UTF-8">
<title>邮箱验证</title>
</head>
<body style="font-family: Arial, sans-serif; max-width: 600px; margin: 0 auto; padding: 20px;">
<h2 style="color: #333;">欢迎注册!</h2>
<p>感谢您注册我们的网站。请点击下面的链接验证您的邮箱地址:</p>
<p><a href="{verification_url}" style="background-color: #007bff; color: white; padding: 10px 20px; text-decoration: none; border-radius: 4px;">验证邮箱</a></p>
<p>如果按钮无法点击,请将以下链接复制到浏览器地址栏:</p>
<p style="word-break: break-all;">{verification_url}</p>
<p style="color: #666; font-size: 14px;">此链接24小时内有效。</p>
</body>
</html>
',
'password_reset' => '
<html>
<head>
<meta charset="UTF-8">
<title>密码重置</title>
</head>
<body style="font-family: Arial, sans-serif; max-width: 600px; margin: 0 auto; padding: 20px;">
<h2 style="color: #333;">重置密码</h2>
<p>您请求重置密码。请点击下面的链接重置您的密码:</p>
<p><a href="{reset_url}" style="background-color: #dc3545; color: white; padding: 10px 20px; text-decoration: none; border-radius: 4px;">重置密码</a></p>
<p>如果按钮无法点击,请将以下链接复制到浏览器地址栏:</p>
<p style="word-break: break-all;">{reset_url}</p>
<p style="color: #666; font-size: 14px;">此链接1小时内有效。</p>
<p style="color: #666; font-size: 14px;">如果您没有请求重置密码,请忽略此邮件。</p>
</body>
</html>
'
];
$template = $templates[$type] ?? '';
// 替换模板变量
foreach ($data as $key => $value) {
$template = str_replace('{' . $key . '}', $value, $template);
}
return $template;
}
}
API接口
1. 注册API (public/api/register.php)
<?php
require_once __DIR__ . '/../src/controllers/AuthController.php';
header('Content-Type: application/json');
$controller = new AuthController();
$controller->doRegister();
2. 登录API (public/api/login.php)
<?php
require_once __DIR__ . '/../src/controllers/AuthController.php';
header('Content-Type: application/json');
$controller = new AuthController();
$controller->doLogin();
3. 忘记登录功能
// 在AuthController中添加
public function checkRememberLogin() {
if ($this->getSession('user_id')) {
return true;
}
$token = $_COOKIE['remember_token'] ?? null;
if (!$token) {
return false;
}
$sql = "SELECT id FROM users WHERE remember_token = ? AND status = 'active'";
$stmt = $this->db->prepare($sql);
$stmt->execute([$token]);
$user = $stmt->fetch();
if ($user) {
$this->createSession($user, true);
return true;
}
return false;
}
部署和测试
1. 部署步骤
# 1. 配置Apache
# 确保 .htaccess 文件正确配置
# 2. 设置目录权限
chmod 755 public/
chmod 755 src/
chmod 644 public/*.php
chmod 644 src/**/*.php
chmod 777 logs/
chmod 777 uploads/
# 3. 创建数据库
mysql -u root -p < database/create_tables.sql
# 4. 配置数据库连接
# 编辑 src/config/database.php 中的数据库配置
# 5. 测试功能
# 访问 http://yourdomain.com/ 开始测试
2. 功能测试清单
## 测试清单
### 注册功能
- [ ] 正常注册
- [ ] 邮箱重复注册
- [ ] 用户名重复注册
- [ ] 密码过短
- [ ] 密码不一致
- [ ] 邮件验证功能
### 登录功能
- [ ] 正常登录
- [ ] 错误密码登录
- [ ] 不存在的用户登录
- [ ] 账户未激活登录
- [ ] 记住登录功能
### 密码重置
- [ ] 发送重置邮件
- [ ] 验证重置链接
- [ ] 重置密码功能
- [ ] 链接过期处理
### 安全性
- [ ] SQL注入防护
- [ ] XSS防护
- [ ] CSRF防护
- [ ] 密码哈希存储
- [ ] 登录失败锁定
扩展功能建议
1. 增强功能
- 手机号注册
- 第三方登录(微信、QQ、GitHub)
- 双因素认证(2FA)
- 用户等级系统
- 积分系统
- 邀请码注册
2. 管理功能
- 管理员后台
- 用户管理
- 权限管理
- 操作日志
- 数据统计
3. 安全增强
- IP白名单
- 设备管理
- 异地登录提醒
- 密码强度要求
- 账户安全评分
总结
通过这个用户注册登录系统项目,你已经实践了:
- MVC架构设计:学会了如何组织代码结构
- 数据库操作:掌握了PDO和预处理语句
- 表单处理:学会了表单验证和错误处理
- 会话管理:理解了Session和Cookie的使用
- 安全编程:实践了各种安全防护措施
- 邮件发送:学会了邮件功能的实现
- 项目规划:掌握了完整的开发流程
这个项目为你后续的Web开发打下了坚实的基础。你可以基于这个项目继续开发更复杂的应用,如博客系统、电商网站等。
记住,安全性是Web应用的生命线。在实际开发中,要时刻保持安全意识,遵循最佳实践,保护用户的数据和隐私。