第 2 章:用户认证系统与产品管理模块开发

章节介绍

学习目标

本章将带领学习者掌握用户认证系统的完整开发流程,包括注册、登录、密码重置等核心功能,同时实现后台产品管理模块,涵盖产品 CRUD 操作、图片上传和搜索分页等功能。通过本章学习,你将能够构建安全可靠的用户系统和高效的产品管理后台。

在整个教程中的作用

用户认证和产品管理是电商系统的两大基石。本章承上启下,在第 1 章搭建的开发环境基础上,开始实现具体的业务功能,为后续的购物车、订单处理等高级功能奠定基础。没有完善的用户系统和产品管理,就无法构建完整的电商生态。

与前面章节的衔接

在第 1 章中,我们已经完成了开发环境配置、MVC 项目结构搭建和数据库设计。本章将直接使用已创建的用户表(users)、产品表(products)等数据表结构,基于 MVC 架构实现具体的业务逻辑。

本章主要内容概览

  1. 用户会话管理与安全机制
  2. 完整的用户注册登录系统
  3. 密码加密与重置功能
  4. 产品管理后台开发
  5. 文件上传与图片处理
  6. 数据验证与安全防护
  7. 分页搜索功能实现

核心概念讲解

用户会话管理与安全性

会话(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 防护

数据验证层次:

  1. 客户端验证:提高用户体验,但不可靠
  2. 服务器端验证:必须进行,确保数据安全
  3. 数据库约束:最后防线
    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;
    }
}

实战项目

项目需求分析和技术方案

项目名称: 完整电商用户系统与产品管理后台
功能需求:

  1. 用户注册、登录、退出系统
  2. 密码重置功能
  3. 用户资料管理
  4. 后台产品管理(增删改查)
  5. 产品图片上传与处理
  6. 产品搜索与分页展示
    技术方案:
  • 采用 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;
    }
}

项目测试和部署指南

测试要点:
  1. 用户注册测试
    • 测试正常注册流程
  • 测试重复用户名/邮箱
  • 测试弱密码验证
  • 测试 CSRF 防护
  1. 登录安全测试
    • 测试错误密码限制
  • 测试会话安全性
  • 测试记住我功能
  1. 产品管理测试
    • 测试图片上传各种格式
  • 测试文件大小限制
  • 测试 SQL 注入防护
  • 测试 XSS 防护
部署步骤:
  1. 配置 Web 服务器(Apache/Nginx)
  2. 设置数据库连接参数
  3. 配置文件上传目录权限
  4. 设置环境变量和安全配置
  5. 运行数据库迁移脚本

项目扩展和优化建议

  1. 功能扩展
    • 实现用户邮箱验证
  • 添加社交登录功能
  • 实现产品评论和评分
  • 添加产品收藏功能
  1. 性能优化
    • 实现数据库查询缓存
  • 使用 Redis 缓存会话数据
  • 图片 CDN 加速
  • 数据库索引优化
  1. 安全增强
    • 实现双因素认证
  • 添加操作日志记录
  • 实现 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
    {
        // 数据验证和业务逻辑
}
}

常见错误和避坑指南

  1. 安全相关错误:
    • 忘记验证用户输入
  • 在错误消息中泄露敏感信息
  • 使用弱加密算法
  • 会话管理不当
  1. 性能相关错误:
    • N+1 查询问题
  • 未使用数据库索引
  • 大文件上传内存溢出
  • 未实现分页查询
  1. 代码质量错误:
    • 重复代码
  • 过长的函数和方法
  • 缺乏错误处理
  • 不合理的依赖关系

性能优化技巧

  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);
  1. 图片优化:
// 使用WebP格式替代JPEG/PNG
if (function_exists('imagewebp')) {
    imagewebp($image, $path, 80); // 80% 质量
}
  1. 会话优化:
// 使用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
        );
    }
}

练习题与挑战

基础练习题

  1. 用户注册表单验证
    • 题目描述:实现一个完整的用户注册表单,包含用户名、邮箱、密码、确认密码字段,实现前后端验证。
  • 难度等级:★☆☆☆☆
  • 解题提示:使用 JavaScript 进行前端验证,PHP 进行后端验证,确保密码强度和安全。
  1. 会话管理实现
    • 题目描述:实现用户登录后的会话管理,包括会话创建、验证和销毁。
  • 难度等级:★☆☆☆☆
  • 解题提示:使用 PHP 的 session 机制,注意会话安全和超时处理。

进阶练习题

  1. 图片上传安全加固
    • 题目描述:改进图片上传功能,增加文件类型验证、病毒扫描和图片压缩。
  • 难度等级:★★★☆☆
  • 解题提示:使用 finfo 验证 MIME 类型,集成 ClamAV 进行病毒扫描,使用 GD 库进行图片压缩。
  1. 产品搜索优化
    • 题目描述:实现高效的产品搜索功能,支持关键词搜索、分类过滤、价格区间和排序。
  • 难度等级:★★☆☆☆
  • 解题提示:使用 MySQL 的全文索引,实现复合查询条件,注意 SQL 注入防护。

综合挑战题

  1. 完整用户管理系统
    • 题目描述:开发一个完整的用户管理系统,包含注册、登录、资料修改、密码重置、邮箱验证等功能。
  • 难度等级:★★★★☆
  • 解题提示:采用 MVC 架构,实现所有安全防护措施,包括 CSRF、XSS、SQL 注入防护。
  1. 后台产品管理平台
    • 题目描述:构建功能完善的产品管理后台,支持产品 CRUD、批量操作、图片管理、数据导出。
  • 难度等级:★★★★★
  • 解题提示:使用 AJAX 实现无刷新操作,实现文件分片上传,添加操作日志记录。

章节总结

本章重点知识回顾

  1. 用户认证系统
    • 安全的用户注册和登录实现
  • 密码哈希和验证最佳实践
  • 会话管理和安全防护
  • 密码重置功能实现
  1. 产品管理模块
    • 产品数据的 CRUD 操作
  • 图片上传和安全处理
  • 分页查询和搜索功能
  • 后台管理界面设计
  1. 安全防护体系
    • SQL 注入攻击原理和防护
  • XSS 跨站脚本防护
  • CSRF 跨站请求伪造防护
  • 文件上传安全处理

技能掌握要求

完成本章学习后,你应该能够:

  • 独立开发完整的用户认证系统
  • 实现安全的产品管理功能
  • 理解和应用 Web 安全防护措施
  • 处理文件上传和图片优化
  • 设计合理的数据库查询和分页

与下一章的衔接预告

在第 3 章中,我们将深入探讨购物车系统和订单处理流程。你将学习:

  • 会话存储的购物车实现
  • AJAX 异步更新购物车
  • 订单状态机设计
  • 库存管理和订单流程
  • 邮件通知系统集成

进一步学习建议

  1. 安全深度研究
    • OWASP Top 10 安全风险
  • Web 应用防火墙配置
  • 安全代码审计工具
  1. 性能优化进阶
    • 数据库查询优化
  • 缓存策略设计
  • 前端性能优化
  1. 框架学习
    • Laravel 或 Symfony 框架
  • 现代 PHP 开发实践
  • Composer 依赖管理
    通过本章的学习,你已经掌握了电商系统最基础也是最重要的两个模块。这些知识不仅适用于电商开发,也是任何 Web 应用开发的核心技能。在继续下一章之前,请确保你已充分理解并能够独立实现本章的所有功能。
Logo

电商企业物流数字化转型必备!快递鸟 API 接口,72 小时快速完成物流系统集成。全流程实战1V1指导,营造开放的API技术生态圈。

更多推荐