实战 PHP 电商开发:从零构建完整在线商店-2
本章详细介绍了电商系统核心模块的开发,重点讲解了用户认证系统和产品管理功能的实现。主要内容包括:用户注册/登录流程的安全实现、密码哈希加密、CSRF防护机制、文件上传处理等核心安全技术。通过完整的代码示例展示了用户会话管理、密码验证、表单数据校验等关键功能的实现方法,为构建安全可靠的电商系统奠定了基础。本章内容衔接前章开发环境配置,为后续购物车、订单等高级功能开发做好准备。
第 2 章:用户认证系统与产品管理模块开发
章节介绍
学习目标
本章将带领学习者掌握用户认证系统的完整开发流程,包括注册、登录、密码重置等核心功能,同时实现后台产品管理模块,涵盖产品 CRUD 操作、图片上传和搜索分页等功能。通过本章学习,你将能够构建安全可靠的用户系统和高效的产品管理后台。
在整个教程中的作用
用户认证和产品管理是电商系统的两大基石。本章承上启下,在第 1 章搭建的开发环境基础上,开始实现具体的业务功能,为后续的购物车、订单处理等高级功能奠定基础。没有完善的用户系统和产品管理,就无法构建完整的电商生态。
与前面章节的衔接
在第 1 章中,我们已经完成了开发环境配置、MVC 项目结构搭建和数据库设计。本章将直接使用已创建的用户表(users)、产品表(products)等数据表结构,基于 MVC 架构实现具体的业务逻辑。
本章主要内容概览
- 用户会话管理与安全机制
- 完整的用户注册登录系统
- 密码加密与重置功能
- 产品管理后台开发
- 文件上传与图片处理
- 数据验证与安全防护
- 分页搜索功能实现
核心概念讲解
用户会话管理与安全性
会话(Session)是 Web 应用中维护用户状态的核心机制。PHP 通过 session_start()函数开启会话,使用$_SESSION 超全局数组存储用户数据。
安全考虑要点:
- 会话固定攻击:每次登录后重新生成 session_id
- 会话劫持:使用 HTTPS、设置 HttpOnly cookie 标志
- 会话超时:设置合理的会话过期时间
- 会话数据安全:避免在 session 中存储敏感信息
// 安全的会话配置
ini_set('session.cookie_httponly', 1);
ini_set('session.cookie_secure', 1); // 仅在HTTPS下使用
ini_set('session.use_strict_mode', 1);
密码哈希加密与验证
密码绝对不能以明文形式存储。PHP 提供了 password_hash()和 password_verify()函数来处理密码安全。
最佳实践:
- 使用 PASSWORD_DEFAULT 算法(当前为 bcrypt)
- 自动生成 salt,无需手动设置
- 验证时使用 password_verify(),不要直接比较哈希值
// 创建密码哈希
$hashedPassword = password_hash($password, PASSWORD_DEFAULT);
// 验证密码
if (password_verify($inputPassword, $storedHash)) {
// 密码正确
}
表单数据验证与 CSRF 防护
数据验证层次:
- 客户端验证:提高用户体验,但不可靠
- 服务器端验证:必须进行,确保数据安全
- 数据库约束:最后防线
CSRF 防护原理:
CSRF(跨站请求伪造)攻击利用用户已登录的状态执行非法操作。防护方法是在表单中嵌入随机令牌,服务器验证令牌有效性。
文件上传处理与图片优化
安全上传要点:
- 验证文件类型(MIME 类型和扩展名)
- 限制文件大小
- 重命名上传文件,避免目录遍历
- 存储上传文件在 Web 根目录之外
- 对图片进行压缩和缩略图生成
代码示例
示例 1:用户注册功能实现
<?php
// controllers/RegisterController.php
class RegisterController {
private $userModel;
private $validator;
public function __construct() {
$this->userModel = new UserModel();
$this->validator = new Validator();
}
public function showRegisterForm() {
// 生成CSRF令牌
$csrfToken = bin2hex(random_bytes(32));
$_SESSION['csrf_token'] = $csrfToken;
// 显示注册页面
require 'views/auth/register.php';
}
public function handleRegistration() {
// 验证CSRF令牌
if (!isset($_POST['csrf_token']) || $_POST['csrf_token'] !== $_SESSION['csrf_token']) {
$_SESSION['error'] = '非法请求';
header('Location: /register');
exit;
}
// 获取并清理输入数据
$username = trim($_POST['username']);
$email = filter_var(trim($_POST['email']), FILTER_SANITIZE_EMAIL);
$password = $_POST['password'];
$confirmPassword = $_POST['confirm_password'];
// 数据验证
$errors = [];
// 用户名验证:3-20个字符,只允许字母数字和下划线
if (!$this->validator->validateUsername($username)) {
$errors['username'] = '用户名必须为3-20个字符,只能包含字母、数字和下划线';
}
// 邮箱验证
if (!$this->validator->validateEmail($email)) {
$errors['email'] = '请输入有效的邮箱地址';
}
// 密码强度验证
if (!$this->validator->validatePassword($password)) {
$errors['password'] = '密码必须至少8位,包含字母和数字';
}
// 确认密码匹配
if ($password !== $confirmPassword) {
$errors['confirm_password'] = '两次输入的密码不一致';
}
// 检查用户名和邮箱是否已存在
if ($this->userModel->isUsernameExists($username)) {
$errors['username'] = '用户名已存在';
}
if ($this->userModel->isEmailExists($email)) {
$errors['email'] = '邮箱已被注册';
}
// 如果有错误,返回注册页面显示错误
if (!empty($errors)) {
$_SESSION['form_errors'] = $errors;
$_SESSION['old_input'] = $_POST;
header('Location: /register');
exit;
}
// 创建用户
$userId = $this->userModel->createUser([
'username' => $username,
'email' => $email,
'password' => password_hash($password, PASSWORD_DEFAULT),
'created_at' => date('Y-m-d H:i:s')
]);
if ($userId) {
// 注册成功,发送欢迎邮件
$this->sendWelcomeEmail($email, $username);
// 清除CSRF令牌
unset($_SESSION['csrf_token']);
$_SESSION['success'] = '注册成功,请登录';
header('Location: /login');
exit;
} else {
$_SESSION['error'] = '注册失败,请稍后重试';
header('Location: /register');
exit;
}
}
private function sendWelcomeEmail($email, $username) {
// 实现邮件发送逻辑
$subject = '欢迎注册我们的电商平台';
$message = "亲爱的 {$username},\n\n感谢您注册我们的电商平台!";
// mail($email, $subject, $message);
}
}
示例 2:用户登录与会话管理
<?php
// controllers/LoginController.php
class LoginController {
private $userModel;
private $validator;
public function __construct() {
$this->userModel = new UserModel();
$this->validator = new Validator();
}
public function showLoginForm() {
// 如果用户已登录,重定向到首页
if (isset($_SESSION['user_id'])) {
header('Location: /');
exit;
}
$csrfToken = bin2hex(random_bytes(32));
$_SESSION['csrf_token'] = $csrfToken;
require 'views/auth/login.php';
}
public function handleLogin() {
// CSRF防护
if (!isset($_POST['csrf_token']) || $_POST['csrf_token'] !== $_SESSION['csrf_token']) {
$_SESSION['error'] = '非法请求';
header('Location: /login');
exit;
}
$username = trim($_POST['username']);
$password = $_POST['password'];
$remember = isset($_POST['remember']);
// 基本验证
if (empty($username) || empty($password)) {
$_SESSION['error'] = '请输入用户名和密码';
header('Location: /login');
exit;
}
// 获取用户信息
$user = $this->userModel->getUserByUsername($username);
if (!$user || !password_verify($password, $user['password'])) {
// 记录登录失败尝试
$this->logFailedAttempt($_SERVER['REMOTE_ADDR'], $username);
$_SESSION['error'] = '用户名或密码错误';
header('Location: /login');
exit;
}
// 检查账户是否被锁定(防止暴力破解)
if ($this->isAccountLocked($user['id'])) {
$_SESSION['error'] = '账户已被临时锁定,请稍后重试';
header('Location: /login');
exit;
}
// 登录成功,设置会话
session_regenerate_id(true); // 防止会话固定攻击
$_SESSION['user_id'] = $user['id'];
$_SESSION['username'] = $user['username'];
$_SESSION['role'] = $user['role'];
$_SESSION['login_time'] = time();
// 更新最后登录时间
$this->userModel->updateLastLogin($user['id']);
// 清除失败尝试记录
$this->clearFailedAttempts($user['id']);
// 记住我功能
if ($remember) {
$this->setRememberMeCookie($user['id']);
}
// 清除CSRF令牌
unset($_SESSION['csrf_token']);
// 重定向到之前访问的页面或首页
$redirect = $_SESSION['redirect_after_login'] ?? '/';
unset($_SESSION['redirect_after_login']);
header("Location: {$redirect}");
exit;
}
private function logFailedAttempt($ip, $username) {
// 记录登录失败尝试,用于防止暴力破解
$stmt = $this->userModel->db->prepare(
"INSERT INTO login_attempts (ip_address, username, attempt_time) VALUES (?, ?, NOW())"
);
$stmt->execute([$ip, $username]);
}
private function isAccountLocked($userId) {
// 检查最近15分钟内的失败尝试次数
$stmt = $this->userModel->db->prepare(
"SELECT COUNT(*) FROM login_attempts
WHERE user_id = ? AND attempt_time > DATE_SUB(NOW(), INTERVAL 15 MINUTE)"
);
$stmt->execute([$userId]);
$attempts = $stmt->fetchColumn();
return $attempts >= 5; // 15分钟内5次失败尝试则锁定
}
}
示例 3:SQL 注入攻击与防护
<?php
// 演示SQL注入攻击和防护
class ProductModel {
private $db;
public function __construct() {
$this->db = Database::getConnection();
}
// 易受SQL注入攻击的代码(错误示范)
public function searchProductsVulnerable($keyword) {
$sql = "SELECT * FROM products WHERE name LIKE '%$keyword%' OR description LIKE '%$keyword%'";
$result = $this->db->query($sql);
return $result->fetchAll(PDO::FETCH_ASSOC);
}
// 安全的参数化查询(正确做法)
public function searchProductsSecure($keyword) {
$sql = "SELECT * FROM products WHERE name LIKE ? OR description LIKE ?";
$stmt = $this->db->prepare($sql);
$searchTerm = "%$keyword%";
$stmt->execute([$searchTerm, $searchTerm]);
return $stmt->fetchAll(PDO::FETCH_ASSOC);
}
// 模拟SQL注入攻击
public function demonstrateSqlInjection() {
// 恶意输入
$maliciousInput = "'; DROP TABLE users; --";
echo "恶意输入: " . $maliciousInput . "\n";
// 脆弱代码的执行(实际环境中不要执行)
// $vulnerableResult = $this->searchProductsVulnerable($maliciousInput);
// 这将生成SQL: SELECT * FROM products WHERE name LIKE ''; DROP TABLE users; --'
// 安全代码的执行
$secureResult = $this->searchProductsSecure($maliciousInput);
// 这将安全地处理输入,不会执行DROP语句
return $secureResult;
}
}
// 使用示例
$productModel = new ProductModel();
// 攻击演示(仅用于教学)
// $productModel->demonstrateSqlInjection();
示例 4:产品图片上传处理
<?php
// utils/ImageUploader.php
class ImageUploader {
private $allowedTypes = ['image/jpeg', 'image/png', 'image/gif', 'image/webp'];
private $maxFileSize = 5 * 1024 * 1024; // 5MB
private $uploadPath;
public function __construct($uploadPath = 'uploads/products/') {
$this->uploadPath = $uploadPath;
// 创建上传目录
if (!is_dir($this->uploadPath)) {
mkdir($this->uploadPath, 0755, true);
}
}
public function uploadProductImage($file) {
$errors = [];
// 检查文件上传错误
if ($file['error'] !== UPLOAD_ERR_OK) {
$errors[] = $this->getUploadError($file['error']);
return ['success' => false, 'errors' => $errors];
}
// 验证文件类型
$finfo = finfo_open(FILEINFO_MIME_TYPE);
$mimeType = finfo_file($finfo, $file['tmp_name']);
finfo_close($finfo);
if (!in_array($mimeType, $this->allowedTypes)) {
$errors[] = '不支持的文件类型,仅支持JPG、PNG、GIF和WebP格式';
}
// 验证文件大小
if ($file['size'] > $this->maxFileSize) {
$errors[] = '文件大小不能超过5MB';
}
// 验证是否为真实图片
if (!getimagesize($file['tmp_name'])) {
$errors[] = '上传的文件不是有效的图片';
}
if (!empty($errors)) {
return ['success' => false, 'errors' => $errors];
}
// 生成安全文件名
$extension = pathinfo($file['name'], PATHINFO_EXTENSION);
$filename = $this->generateSafeFilename($extension);
$filepath = $this->uploadPath . $filename;
// 移动上传文件
if (move_uploaded_file($file['tmp_name'], $filepath)) {
// 生成缩略图
$thumbnail = $this->createThumbnail($filepath, 300, 300);
return [
'success' => true,
'filename' => $filename,
'filepath' => $filepath,
'thumbnail' => $thumbnail
];
} else {
$errors[] = '文件上传失败';
return ['success' => false, 'errors' => $errors];
}
}
private function generateSafeFilename($extension) {
// 使用随机字符串重命名文件,避免文件名冲突和路径遍历攻击
$randomString = bin2hex(random_bytes(16));
return $randomString . '.' . $extension;
}
private function createThumbnail($sourcePath, $maxWidth, $maxHeight) {
list($origWidth, $origHeight, $type) = getimagesize($sourcePath);
// 计算缩略图尺寸
$ratio = $origWidth / $origHeight;
if ($maxWidth / $maxHeight > $ratio) {
$newWidth = $maxHeight * $ratio;
$newHeight = $maxHeight;
} else {
$newWidth = $maxWidth;
$newHeight = $maxWidth / $ratio;
}
// 创建图像资源
switch ($type) {
case IMAGETYPE_JPEG:
$source = imagecreatefromjpeg($sourcePath);
break;
case IMAGETYPE_PNG:
$source = imagecreatefrompng($sourcePath);
break;
case IMAGETYPE_GIF:
$source = imagecreatefromgif($sourcePath);
break;
case IMAGETYPE_WEBP:
$source = imagecreatefromwebp($sourcePath);
break;
default:
return false;
}
// 创建缩略图
$thumbnail = imagecreatetruecolor($newWidth, $newHeight);
// 保持透明度(PNG和GIF)
if ($type == IMAGETYPE_PNG || $type == IMAGETYPE_GIF) {
imagecolortransparent($thumbnail, imagecolorallocatealpha($thumbnail, 0, 0, 0, 127));
imagealphablending($thumbnail, false);
imagesavealpha($thumbnail, true);
}
// 调整尺寸
imagecopyresampled($thumbnail, $source, 0, 0, 0, 0, $newWidth, $newHeight, $origWidth, $origHeight);
// 保存缩略图
$thumbPath = $this->uploadPath . 'thumb_' . basename($sourcePath);
switch ($type) {
case IMAGETYPE_JPEG:
imagejpeg($thumbnail, $thumbPath, 85);
break;
case IMAGETYPE_PNG:
imagepng($thumbnail, $thumbPath);
break;
case IMAGETYPE_GIF:
imagegif($thumbnail, $thumbPath);
break;
case IMAGETYPE_WEBP:
imagewebp($thumbnail, $thumbPath, 85);
break;
}
imagedestroy($source);
imagedestroy($thumbnail);
return $thumbPath;
}
private function getUploadError($errorCode) {
switch ($errorCode) {
case UPLOAD_ERR_INI_SIZE:
return '上传的文件超过了php.ini中upload_max_filesize限制';
case UPLOAD_ERR_FORM_SIZE:
return '上传的文件超过了HTML表单中MAX_FILE_SIZE限制';
case UPLOAD_ERR_PARTIAL:
return '文件只有部分被上传';
case UPLOAD_ERR_NO_FILE:
return '没有文件被上传';
case UPLOAD_ERR_NO_TMP_DIR:
return '找不到临时文件夹';
case UPLOAD_ERR_CANT_WRITE:
return '文件写入失败';
default:
return '未知上传错误';
}
}
}
示例 5:产品分页与搜索功能
<?php
// models/ProductModel.php
class ProductModel {
private $db;
public function __construct() {
$this->db = Database::getConnection();
}
public function getProductsPaginated($page = 1, $perPage = 12, $search = '', $category = '') {
$offset = ($page - 1) * $perPage;
// 构建查询条件
$whereConditions = [];
$params = [];
if (!empty($search)) {
$whereConditions[] = "(name LIKE ? OR description LIKE ?)";
$searchTerm = "%$search%";
$params[] = $searchTerm;
$params[] = $searchTerm;
}
if (!empty($category)) {
$whereConditions[] = "category_id = ?";
$params[] = $category;
}
$whereClause = '';
if (!empty($whereConditions)) {
$whereClause = 'WHERE ' . implode(' AND ', $whereConditions);
}
// 获取总记录数
$countSql = "SELECT COUNT(*) FROM products $whereClause";
$countStmt = $this->db->prepare($countSql);
$countStmt->execute($params);
$totalRecords = $countStmt->fetchColumn();
// 获取分页数据
$sql = "SELECT p.*, c.name as category_name
FROM products p
LEFT JOIN categories c ON p.category_id = c.id
$whereClause
ORDER BY p.created_at DESC
LIMIT ? OFFSET ?";
$params[] = $perPage;
$params[] = $offset;
$stmt = $this->db->prepare($sql);
$stmt->execute($params);
$products = $stmt->fetchAll(PDO::FETCH_ASSOC);
return [
'products' => $products,
'totalRecords' => $totalRecords,
'totalPages' => ceil($totalRecords / $perPage),
'currentPage' => $page,
'perPage' => $perPage
];
}
public function searchProducts($keyword, $filters = []) {
$sql = "SELECT * FROM products WHERE 1=1";
$params = [];
// 关键词搜索
if (!empty($keyword)) {
$sql .= " AND (name LIKE ? OR description LIKE ?)";
$searchTerm = "%$keyword%";
$params[] = $searchTerm;
$params[] = $searchTerm;
}
// 价格范围过滤
if (isset($filters['min_price']) && is_numeric($filters['min_price'])) {
$sql .= " AND price >= ?";
$params[] = $filters['min_price'];
}
if (isset($filters['max_price']) && is_numeric($filters['max_price'])) {
$sql .= " AND price <= ?";
$params[] = $filters['max_price'];
}
// 分类过滤
if (!empty($filters['category_id'])) {
$sql .= " AND category_id = ?";
$params[] = $filters['category_id'];
}
// 库存状态
if (isset($filters['in_stock']) && $filters['in_stock']) {
$sql .= " AND stock > 0";
}
$sql .= " ORDER BY created_at DESC";
$stmt = $this->db->prepare($sql);
$stmt->execute($params);
return $stmt->fetchAll(PDO::FETCH_ASSOC);
}
}
// 分页辅助类
class Pagination {
public static function generate($currentPage, $totalPages, $urlPattern) {
if ($totalPages <= 1) return '';
$html = '<nav><ul class="pagination">';
// 上一页
if ($currentPage > 1) {
$html .= '<li class="page-item">';
$html .= '<a class="page-link" href="' . sprintf($urlPattern, $currentPage - 1) . '">上一页</a>';
$html .= '</li>';
}
// 页码
$startPage = max(1, $currentPage - 2);
$endPage = min($totalPages, $startPage + 4);
if ($endPage - $startPage < 4) {
$startPage = max(1, $endPage - 4);
}
for ($i = $startPage; $i <= $endPage; $i++) {
$active = $i == $currentPage ? ' active' : '';
$html .= '<li class="page-item' . $active . '">';
$html .= '<a class="page-link" href="' . sprintf($urlPattern, $i) . '">' . $i . '</a>';
$html .= '</li>';
}
// 下一页
if ($currentPage < $totalPages) {
$html .= '<li class="page-item">';
$html .= '<a class="page-link" href="' . sprintf($urlPattern, $currentPage + 1) . '">下一页</a>';
$html .= '</li>';
}
$html .= '</ul></nav>';
return $html;
}
}
实战项目
项目需求分析和技术方案
项目名称: 完整电商用户系统与产品管理后台
功能需求:
- 用户注册、登录、退出系统
- 密码重置功能
- 用户资料管理
- 后台产品管理(增删改查)
- 产品图片上传与处理
- 产品搜索与分页展示
技术方案:
- 采用 MVC 架构模式
- 使用 PDO 进行数据库操作,防止 SQL 注入
- 实现 CSRF 防护和 XSS 过滤
- 使用 GD 库进行图片处理
- 采用 session 进行用户状态管理
- 实现前端表单验证和后端数据验证
分步骤实现代码和详细说明
步骤 1:数据库表结构设计
-- 用户表
CREATE TABLE users (
id INT AUTO_INCREMENT PRIMARY KEY,
username VARCHAR(50) UNIQUE NOT NULL,
email VARCHAR(100) UNIQUE NOT NULL,
password VARCHAR(255) NOT NULL,
role ENUM('customer', 'admin') DEFAULT 'customer',
first_name VARCHAR(50),
last_name VARCHAR(50),
avatar VARCHAR(255),
is_active BOOLEAN DEFAULT TRUE,
last_login DATETIME,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
);
-- 产品表
CREATE TABLE products (
id INT AUTO_INCREMENT PRIMARY KEY,
name VARCHAR(255) NOT NULL,
description TEXT,
price DECIMAL(10,2) NOT NULL,
compare_price DECIMAL(10,2),
cost_price DECIMAL(10,2),
sku VARCHAR(100) UNIQUE,
barcode VARCHAR(100),
stock INT DEFAULT 0,
weight DECIMAL(8,2),
dimensions VARCHAR(100),
image_path VARCHAR(255),
category_id INT,
is_featured BOOLEAN DEFAULT FALSE,
is_active BOOLEAN DEFAULT TRUE,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
FOREIGN KEY (category_id) REFERENCES categories(id)
);
-- 密码重置令牌表
CREATE TABLE password_resets (
id INT AUTO_INCREMENT PRIMARY KEY,
email VARCHAR(100) NOT NULL,
token VARCHAR(255) NOT NULL,
expires_at DATETIME NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
步骤 2:核心模型类实现
<?php
// models/UserModel.php
class UserModel {
private $db;
public function __construct() {
$this->db = Database::getConnection();
}
public function createUser($userData) {
$sql = "INSERT INTO users (username, email, password, first_name, last_name, created_at)
VALUES (?, ?, ?, ?, ?, NOW())";
$stmt = $this->db->prepare($sql);
return $stmt->execute([
$userData['username'],
$userData['email'],
$userData['password'],
$userData['first_name'] ?? '',
$userData['last_name'] ?? ''
]);
}
public function getUserByEmail($email) {
$sql = "SELECT * FROM users WHERE email = ? AND is_active = TRUE";
$stmt = $this->db->prepare($sql);
$stmt->execute([$email]);
return $stmt->fetch(PDO::FETCH_ASSOC);
}
public function updatePassword($userId, $hashedPassword) {
$sql = "UPDATE users SET password = ?, updated_at = NOW() WHERE id = ?";
$stmt = $this->db->prepare($sql);
return $stmt->execute([$hashedPassword, $userId]);
}
public function createPasswordResetToken($email) {
// 删除旧的重置令牌
$this->deletePasswordResetTokens($email);
// 生成新令牌
$token = bin2hex(random_bytes(50));
$expiresAt = date('Y-m-d H:i:s', strtotime('+1 hour'));
$sql = "INSERT INTO password_resets (email, token, expires_at) VALUES (?, ?, ?)";
$stmt = $this->db->prepare($sql);
$stmt->execute([$email, $token, $expiresAt]);
return $token;
}
public function validatePasswordResetToken($token) {
$sql = "SELECT * FROM password_resets WHERE token = ? AND expires_at > NOW()";
$stmt = $this->db->prepare($sql);
$stmt->execute([$token]);
return $stmt->fetch(PDO::FETCH_ASSOC);
}
}
步骤 3:用户认证中间件
<?php
// middleware/AuthMiddleware.php
class AuthMiddleware {
public static function requireAuth() {
if (!isset($_SESSION['user_id'])) {
$_SESSION['redirect_after_login'] = $_SERVER['REQUEST_URI'];
header('Location: /login');
exit;
}
}
public static function requireAdmin() {
self::requireAuth();
if ($_SESSION['role'] !== 'admin') {
http_response_code(403);
echo '无权访问此页面';
exit;
}
}
public static function requireGuest() {
if (isset($_SESSION['user_id'])) {
header('Location: /');
exit;
}
}
}
// 使用示例
// 在需要登录的页面开头调用
AuthMiddleware::requireAuth();
// 在需要管理员权限的页面调用
AuthMiddleware::requireAdmin();
步骤 4:产品管理控制器
<?php
// controllers/admin/ProductController.php
class ProductController {
private $productModel;
private $imageUploader;
public function __construct() {
$this->productModel = new ProductModel();
$this->imageUploader = new ImageUploader();
}
public function index() {
AuthMiddleware::requireAdmin();
$page = $_GET['page'] ?? 1;
$search = $_GET['search'] ?? '';
$data = $this->productModel->getProductsPaginated($page, 10, $search);
require 'views/admin/products/index.php';
}
public function create() {
AuthMiddleware::requireAdmin();
require 'views/admin/products/create.php';
}
public function store() {
AuthMiddleware::requireAdmin();
// CSRF验证
if (!Security::verifyCsrfToken($_POST['csrf_token'])) {
$_SESSION['error'] = '非法请求';
header('Location: /admin/products/create');
exit;
}
$errors = $this->validateProductData($_POST);
// 处理图片上传
$imageData = null;
if (isset($_FILES['image']) && $_FILES['image']['error'] === UPLOAD_ERR_OK) {
$imageData = $this->imageUploader->uploadProductImage($_FILES['image']);
if (!$imageData['success']) {
$errors = array_merge($errors, $imageData['errors']);
}
}
if (!empty($errors)) {
$_SESSION['form_errors'] = $errors;
$_SESSION['old_input'] = $_POST;
header('Location: /admin/products/create');
exit;
}
// 保存产品数据
$productData = [
'name' => trim($_POST['name']),
'description' => trim($_POST['description']),
'price' => floatval($_POST['price']),
'compare_price' => !empty($_POST['compare_price']) ? floatval($_POST['compare_price']) : null,
'stock' => intval($_POST['stock']),
'sku' => trim($_POST['sku']),
'category_id' => intval($_POST['category_id']),
'image_path' => $imageData['filename'] ?? null
];
$productId = $this->productModel->createProduct($productData);
if ($productId) {
$_SESSION['success'] = '产品创建成功';
header('Location: /admin/products');
exit;
} else {
$_SESSION['error'] = '产品创建失败';
header('Location: /admin/products/create');
exit;
}
}
private function validateProductData($data) {
$errors = [];
if (empty(trim($data['name']))) {
$errors['name'] = '产品名称不能为空';
}
if (!is_numeric($data['price']) || floatval($data['price']) <= 0) {
$errors['price'] = '价格必须为大于0的数字';
}
if (!is_numeric($data['stock']) || intval($data['stock']) < 0) {
$errors['stock'] = '库存必须为非负整数';
}
return $errors;
}
}
项目测试和部署指南
测试要点:
- 用户注册测试
- 测试正常注册流程
- 测试重复用户名/邮箱
- 测试弱密码验证
- 测试 CSRF 防护
- 登录安全测试
- 测试错误密码限制
- 测试会话安全性
- 测试记住我功能
- 产品管理测试
- 测试图片上传各种格式
- 测试文件大小限制
- 测试 SQL 注入防护
- 测试 XSS 防护
部署步骤:
- 配置 Web 服务器(Apache/Nginx)
- 设置数据库连接参数
- 配置文件上传目录权限
- 设置环境变量和安全配置
- 运行数据库迁移脚本
项目扩展和优化建议
- 功能扩展
- 实现用户邮箱验证
- 添加社交登录功能
- 实现产品评论和评分
- 添加产品收藏功能
- 性能优化
- 实现数据库查询缓存
- 使用 Redis 缓存会话数据
- 图片 CDN 加速
- 数据库索引优化
- 安全增强
- 实现双因素认证
- 添加操作日志记录
- 实现 API 速率限制
- 定期安全扫描
安全测试和漏洞修复环节
SQL 注入测试:
// 测试用例
$testInputs = [
"'; DROP TABLE users; --",
"1' OR '1'='1",
"1' UNION SELECT username, password FROM users --"
];
foreach ($testInputs as $input) {
$result = $productModel->searchProductsSecure($input);
// 应该返回空结果或正常处理,不会执行恶意SQL
}
XSS 测试:
<!-- 测试XSS防护 -->
<script>
alert("XSS");
</script>
<img src="x" onerror="alert('XSS')" />
文件上传安全测试:
- 尝试上传 PHP 文件
- 测试路径遍历攻击
- 验证 MIME 类型欺骗
最佳实践
行业标准和开发规范
PSR 标准遵循:
- PSR-1:基础编码规范
- PSR-2:编码风格规范
- PSR-4:自动加载规范
- PSR-7:HTTP 消息接口
代码规范示例:
<?php
declare(strict_types=1);
namespace App\Controllers;
use App\Models\UserModel;
use App\Utils\Validator;
/**
* 用户认证控制器
*
* 处理用户注册、登录、退出等认证相关功能
*/
class AuthController
{
private UserModel $userModel;
private Validator $validator;
public function __construct(UserModel $userModel, Validator $validator)
{
$this->userModel = $userModel;
$this->validator = $validator;
}
/**
* 用户注册处理
*
* @param array $data 用户提交的数据
* @return array 处理结果
*/
public function register(array $data): array
{
// 数据验证和业务逻辑
}
}
常见错误和避坑指南
- 安全相关错误:
- 忘记验证用户输入
- 在错误消息中泄露敏感信息
- 使用弱加密算法
- 会话管理不当
- 性能相关错误:
- N+1 查询问题
- 未使用数据库索引
- 大文件上传内存溢出
- 未实现分页查询
- 代码质量错误:
- 重复代码
- 过长的函数和方法
- 缺乏错误处理
- 不合理的依赖关系
性能优化技巧
- 数据库优化:
-- 为常用查询字段添加索引
CREATE INDEX idx_products_category ON products(category_id);
CREATE INDEX idx_products_price ON products(price);
CREATE INDEX idx_users_email ON users(email);
- 图片优化:
// 使用WebP格式替代JPEG/PNG
if (function_exists('imagewebp')) {
imagewebp($image, $path, 80); // 80% 质量
}
- 会话优化:
// 使用Redis存储会话
ini_set('session.save_handler', 'redis');
ini_set('session.save_path', 'tcp:// 127.0.0.1:6379');
安全性考虑和建议
SQL 注入防护深度解析:
攻击案例:
// 脆弱代码
$userId = $_GET['id'];
$sql = "SELECT * FROM users WHERE id = $userId";
$result = $db->query($sql);
// 攻击者输入:1; DROP TABLE users;
// 生成的SQL:SELECT * FROM users WHERE id = 1; DROP TABLE users;
完整防护方案:
// 1. 使用预处理语句
$stmt = $db->prepare("SELECT * FROM users WHERE id = ?");
$stmt->execute([$userId]);
// 2. 使用ORM
$user = User::find($userId);
// 3. 输入验证和过滤
if (!filter_var($userId, FILTER_VALIDATE_INT)) {
throw new InvalidArgumentException('无效的用户ID');
}
XSS 跨站脚本攻击防护:
攻击案例:
// 脆弱代码
echo "欢迎, " . $_GET['name'];
// 攻击者输入:<script>stealCookie()</script>
// 输出:欢迎, <script>stealCookie()</script>
防护方案:
// 1. 输出转义
function escape($data) {
return htmlspecialchars($data, ENT_QUOTES | ENT_HTML5, 'UTF-8');
}
echo "欢迎, " . escape($_GET['name']);
// 2. 使用模板引擎自动转义
// 在Twig中:{{ name }} 会自动转义
CSRF 跨站请求伪造防护:
攻击原理:
攻击者诱导用户点击恶意链接,利用用户已登录的状态执行非法操作。
完整防护方案:
// 1. 生成CSRF令牌
class CSRFProtection {
public static function generateToken(): string {
if (empty($_SESSION['csrf_token'])) {
$_SESSION['csrf_token'] = bin2hex(random_bytes(32));
}
return $_SESSION['csrf_token'];
}
public static function validateToken(string $token): bool {
if (empty($_SESSION['csrf_token']) || empty($token)) {
return false;
}
return hash_equals($_SESSION['csrf_token'], $token);
}
}
// 2. 在表单中使用
<form method="post">
<input type="hidden" name="csrf_token" value="<?= CSRFProtection::generateToken() ?>">
<!-- 其他表单字段 -->
</form>
// 3. 验证令牌
if (!CSRFProtection::validateToken($_POST['csrf_token'])) {
throw new Exception('CSRF令牌验证失败');
}
身份认证和授权安全:
密码安全实践:
class PasswordSecurity {
// 密码强度验证
public static function validateStrength(string $password): bool {
$minLength = 8;
$hasUpperCase = preg_match('/[A-Z]/', $password);
$hasLowerCase = preg_match('/[a-z]/', $password);
$hasNumbers = preg_match('/\d/', $password);
$hasSpecial = preg_match('/[^A-Za-z0-9]/', $password);
return strlen($password) >= $minLength
&& $hasUpperCase && $hasLowerCase
&& $hasNumbers && $hasSpecial;
}
// 防止时序攻击
public static function slowEquals(string $a, string $b): bool {
$diff = strlen($a) ^ strlen($b);
for ($i = 0; $i < strlen($a) && $i < strlen($b); $i++) {
$diff |= ord($a[$i]) ^ ord($b[$i]);
}
return $diff === 0;
}
}
数据加密和传输安全:
敏感数据加密:
class DataEncryption {
private string $key;
private string $cipher = 'aes-256-gcm';
public function __construct(string $key) {
$this->key = $key;
}
public function encrypt(string $data): string {
$iv = random_bytes(openssl_cipher_iv_length($this->cipher));
$tag = '';
$ciphertext = openssl_encrypt(
$data, $this->cipher, $this->key, 0, $iv, $tag
);
return base64_encode($iv . $tag . $ciphertext);
}
public function decrypt(string $data): string {
$data = base64_decode($data);
$ivLength = openssl_cipher_iv_length($this->cipher);
$iv = substr($data, 0, $ivLength);
$tag = substr($data, $ivLength, 16);
$ciphertext = substr($data, $ivLength + 16);
return openssl_decrypt(
$ciphertext, $this->cipher, $this->key, 0, $iv, $tag
);
}
}
练习题与挑战
基础练习题
- 用户注册表单验证
- 题目描述:实现一个完整的用户注册表单,包含用户名、邮箱、密码、确认密码字段,实现前后端验证。
- 难度等级:★☆☆☆☆
- 解题提示:使用 JavaScript 进行前端验证,PHP 进行后端验证,确保密码强度和安全。
- 会话管理实现
- 题目描述:实现用户登录后的会话管理,包括会话创建、验证和销毁。
- 难度等级:★☆☆☆☆
- 解题提示:使用 PHP 的 session 机制,注意会话安全和超时处理。
进阶练习题
- 图片上传安全加固
- 题目描述:改进图片上传功能,增加文件类型验证、病毒扫描和图片压缩。
- 难度等级:★★★☆☆
- 解题提示:使用 finfo 验证 MIME 类型,集成 ClamAV 进行病毒扫描,使用 GD 库进行图片压缩。
- 产品搜索优化
- 题目描述:实现高效的产品搜索功能,支持关键词搜索、分类过滤、价格区间和排序。
- 难度等级:★★☆☆☆
- 解题提示:使用 MySQL 的全文索引,实现复合查询条件,注意 SQL 注入防护。
综合挑战题
- 完整用户管理系统
- 题目描述:开发一个完整的用户管理系统,包含注册、登录、资料修改、密码重置、邮箱验证等功能。
- 难度等级:★★★★☆
- 解题提示:采用 MVC 架构,实现所有安全防护措施,包括 CSRF、XSS、SQL 注入防护。
- 后台产品管理平台
- 题目描述:构建功能完善的产品管理后台,支持产品 CRUD、批量操作、图片管理、数据导出。
- 难度等级:★★★★★
- 解题提示:使用 AJAX 实现无刷新操作,实现文件分片上传,添加操作日志记录。
章节总结
本章重点知识回顾
- 用户认证系统
- 安全的用户注册和登录实现
- 密码哈希和验证最佳实践
- 会话管理和安全防护
- 密码重置功能实现
- 产品管理模块
- 产品数据的 CRUD 操作
- 图片上传和安全处理
- 分页查询和搜索功能
- 后台管理界面设计
- 安全防护体系
- SQL 注入攻击原理和防护
- XSS 跨站脚本防护
- CSRF 跨站请求伪造防护
- 文件上传安全处理
技能掌握要求
完成本章学习后,你应该能够:
- 独立开发完整的用户认证系统
- 实现安全的产品管理功能
- 理解和应用 Web 安全防护措施
- 处理文件上传和图片优化
- 设计合理的数据库查询和分页
与下一章的衔接预告
在第 3 章中,我们将深入探讨购物车系统和订单处理流程。你将学习:
- 会话存储的购物车实现
- AJAX 异步更新购物车
- 订单状态机设计
- 库存管理和订单流程
- 邮件通知系统集成
进一步学习建议
- 安全深度研究
- OWASP Top 10 安全风险
- Web 应用防火墙配置
- 安全代码审计工具
- 性能优化进阶
- 数据库查询优化
- 缓存策略设计
- 前端性能优化
- 框架学习
- Laravel 或 Symfony 框架
- 现代 PHP 开发实践
- Composer 依赖管理
通过本章的学习,你已经掌握了电商系统最基础也是最重要的两个模块。这些知识不仅适用于电商开发,也是任何 Web 应用开发的核心技能。在继续下一章之前,请确保你已充分理解并能够独立实现本章的所有功能。
更多推荐

所有评论(0)