《零基础学 PHP:从入门到实战》·PHP Web 安全开发核心技术与攻防实战演练-安全上传与文件管理

第 5 章:文件操作风险管控——安全上传与文件管理

章节介绍

学习目标

通过本章学习,您将能够:

  1. 深刻理解文件上传功能中潜藏的多重安全风险(如 Webshell 上传、路径遍历等)
  2. 掌握构建多层防御的文件上传安全校验流程
  3. 学会安全地管理用户上传的文件,包括存储、访问和清理
  4. 理解并防范文件系统操作中的目录遍历攻击
  5. 能够在实际项目中实现一个完整的、符合安全标准的文件上传模块

本章作用与定位

在前四章中,我们学习了输入验证、SQL 注入防护、XSS 与 CSRF 防御等 Web 安全基础知识.本章将聚焦于另一个高频攻击面——文件操作.文件上传是 Web 应用中极为常见的功能,也是攻击者最青睐的突破口之一.一个未经严格校验的文件上传点,可能瞬间成为攻击者控制整个服务器的后门.本章将系统性地讲解文件上传漏洞的攻防原理,并指导您构建从客户端到服务器端的完整安全防御体系.

与前面章节的衔接

本章内容与**第 2 章(输入验证)紧密相关,文件上传本质上是特殊类型的用户输入.与第 4 章(XSS 防护)也有交叉,因为恶意文件可能包含脚本代码.同时,安全的文件管理也会用到第 7 章(加密)**中的部分知识.掌握本章内容后,您将对 Web 应用的攻击面有更全面的认识.

本章主要内容概览

  1. 文件上传漏洞深度剖析:分析攻击者如何通过文件上传获取服务器控制权
  2. 安全校验多层防御:从客户端到服务器端的完整校验流程设计
  3. 安全存储与访问控制:文件存储策略、权限设置与访问隔离
  4. 文件系统操作安全:防范目录遍历攻击的最佳实践
  5. 综合实战项目:构建一个带完整安全防护的图片上传相册系统

核心概念讲解

文件上传漏洞的本质与危害

文件上传功能的安全风险源于一个简单的事实:服务器执行了用户可控的文件内容.攻击者通过精心构造,可以上传并执行恶意脚本,从而控制服务器.

主要攻击方式
  1. Webshell 上传:上传包含 PHP、ASP、JSP 等服务器端脚本语言代码的文件,通过浏览器访问该文件即可在服务器上执行任意命令.
  2. 钓鱼文件:上传伪装成正常文件的恶意程序(如.exe、.bat),诱骗其他用户下载执行.
  3. 文件覆盖:通过目录遍历或文件名预测,覆盖服务器上的关键系统文件或应用配置文件.
  4. 拒绝服务攻击:上传超大文件耗尽服务器磁盘空间或处理资源.
  5. 客户端攻击:上传包含恶意脚本的 HTML/JS 文件,当其他用户访问时触发 XSS 攻击.
漏洞利用条件

要使文件上传漏洞被成功利用,通常需要满足以下条件之一:

  • 服务器配置不当,允许直接执行上传目录中的脚本文件
  • 应用程序未对文件内容进行有效校验,仅检查扩展名
  • 存在文件解析漏洞(如 Apache 的mod_mime解析缺陷)
  • 能够结合其他漏洞(如目录遍历)将文件上传到可执行目录

安全的文件校验流程(纵深防御)

单一防御措施极易被绕过,应采用多层防御策略:

客户端校验 → 服务器端扩展名白名单 → MIME类型检查 → 文件头校验 → 内容安全检查 → 随机重命名 → 安全存储
1. 客户端校验(辅助层)
  • 作用:提供即时反馈,提升用户体验,减少无效请求
  • 限制:完全不可信,可被轻易绕过
  • 实现:HTML5 的accept属性、JavaScript 文件类型/大小校验
2. 服务器端扩展名白名单(基础层)
  • 原则:只允许已知安全的扩展名,拒绝其他所有
  • 实现:维护一个小型的、明确允许的扩展名列表(如.jpg, .png, .gif)
  • 注意:不要使用黑名单!攻击者总能找到不在名单中的危险扩展名
3. MIME 类型检查(增强层)
  • 原理:检查 HTTP 请求头中的Content-Type信息
  • 限制:可被篡改,不能单独依赖
  • 实现:通过$_FILES['file']['type']获取,但需结合其他校验
4. 文件头校验(内容层)
  • 原理:检查文件的实际二进制内容开头部分(魔术字节)
  • 优势:难以伪造,是判断文件真实类型的可靠方法
  • 实现:使用getimagesize()检查图片,或直接读取文件头字节
5. 内容安全检查(深度层)
  • 目的:防止图片马(在正常图片中嵌入恶意代码)
  • 方法:对图片进行二次渲染、使用防病毒软件扫描、检查文件内容是否包含 PHP 标签等
6. 随机重命名(隔离层)
  • 目的:防止攻击者预测文件路径,避免文件覆盖攻击
  • 方法:使用不可预测的随机字符串(如 UUID)作为文件名,保留原始扩展名
7. 安全存储(物理层)
  • 原则:上传文件存储在 Web 根目录之外,或通过脚本代理访问
  • 配置:正确设置文件系统权限(最小权限原则)

目录遍历攻击(Path Traversal)

目录遍历攻击通过使用../等路径遍历序列,访问或操作应用程序预期目录之外的文件.

攻击示例
  • 文件下载功能:download.php?file=../../../../etc/passwd
  • 文件上传功能:通过文件名../../../var/www/html/shell.php将文件上传到可执行目录
  • 文件包含功能:include($_GET['page'] . '.php'),传入../../../etc/passwd%00
防护方法
  1. 规范化路径:使用realpath()获取绝对路径,并与允许的基准目录比较
  2. 白名单过滤:只允许文件名,不允许路径
  3. 剥离目录遍历序列:过滤掉../..\等序列
  4. 使用索引存储:将文件存储在数据库中,通过 ID 引用而非直接路径

代码示例

示例 1:存在严重漏洞的文件上传代码(反面教材)

这是一个典型的、存在多个安全漏洞的文件上传实现:

<?php
// 存在严重安全漏洞的文件上传代码 - 请勿在生产环境使用!
// 1. 没有任何输入验证
if(isset($_FILES['uploaded_file'])) {
    $file = $_FILES['uploaded_file'];

    // 2. 直接使用用户提供的文件名 - 存在路径遍历风险
$target_path = "uploads/" . $file['name'];

    // 3. 没有任何文件类型校验
if(move_uploaded_file($file['tmp_name'], $target_path)) {
        echo "文件上传成功: <a href='$target_path'>查看文件</a>";
    } else {
        echo "文件上传失败";
    }
}
?>

攻击演示:

  1. 上传名为shell.php的文件,内容为<?php system($_GET['cmd']); ?>
  2. 访问http:// example.com/uploads/shell.php?cmd=whoami即可执行系统命令
  3. 上传名为../../../var/www/html/shell.php的文件,可能将 Webshell 写入 Web 根目录

示例 2:基础安全防护的文件上传代码

以下是添加了基础安全防护的上传代码:

<?php
// 基础安全防护的文件上传示例
// 定义允许的文件类型白名单
$allowed_extensions = ['jpg', 'jpeg', 'png', 'gif', 'pdf'];
$max_file_size = 2 * 1024 * 1024; // 2MB

// 检查是否有文件上传
if(!isset($_FILES['uploaded_file']) || $_FILES['uploaded_file']['error'] !== UPLOAD_ERR_OK) {
    die("文件上传失败或未选择文件");
}

$file = $_FILES['uploaded_file'];

// 1. 检查文件大小
if($file['size'] > $max_file_size) {
    die("文件大小超过限制(最大2MB)");
}

// 2. 获取文件扩展名并进行白名单校验
$file_name = $file['name'];
$file_ext = strtolower(pathinfo($file_name, PATHINFO_EXTENSION));

if(!in_array($file_ext, $allowed_extensions)) {
    die("不支持的文件类型,仅允许: " . implode(', ', $allowed_extensions));
}

// 3. 生成安全的随机文件名(防止文件覆盖和路径遍历)
$safe_file_name = uniqid('file_', true) . '.' . $file_ext;
$upload_dir = 'uploads/';
$target_path = $upload_dir . $safe_file_name;

// 4. 确保目标目录存在
if(!is_dir($upload_dir)) {
    mkdir($upload_dir, 0755, true);
}

// 5. 移动上传的文件
if(move_uploaded_file($file['tmp_name'], $target_path)) {
    // 6. 设置安全权限(仅所有者可读写,其他人只读)
    chmod($target_path, 0644);

    echo "文件上传成功!<br>";
    echo "保存为: " . htmlspecialchars($safe_file_name) . "<br>";

    // 仅对图片文件显示预览
if(in_array($file_ext, ['jpg', 'jpeg', 'png', 'gif'])) {
        echo "<img src='$target_path' style='max-width: 300px;' alt='上传的图片'>";
    }
} else {
    die("文件保存失败");
}
?>

示例 3:包含文件头校验的增强版上传代码

<?php
// 包含文件头校验的增强安全上传
class SecureFileUploader {
    private $allowed_extensions = ['jpg', 'jpeg', 'png', 'gif'];
    private $allowed_mime_types = ['image/jpeg', 'image/png', 'image/gif'];
    private $max_file_size = 2 * 1024 * 1024; // 2MB
    private $upload_dir = 'secure_uploads/';

    // 图片文件的魔术字节签名
private $image_signatures = [
        'jpg' => "\xFF\xD8\xFF",
        'png' => "\x89\x50\x4E\x47",
        'gif' => "GIF89a"
    ];

    public function upload($file_field_name) {
        // 验证上传状态
if(!isset($_FILES[$file_field_name])) {
            throw new Exception('没有文件被上传');
        }

        $file = $_FILES[$file_field_name];

        // 检查上传错误码
if($file['error'] !== UPLOAD_ERR_OK) {
            $error_messages = [
                UPLOAD_ERR_INI_SIZE => '文件大小超过服务器限制',
                UPLOAD_ERR_FORM_SIZE => '文件大小超过表单限制',
                UPLOAD_ERR_PARTIAL => '文件只有部分被上传',
                UPLOAD_ERR_NO_FILE => '没有文件被上传',
                UPLOAD_ERR_NO_TMP_DIR => '缺少临时文件夹',
                UPLOAD_ERR_CANT_WRITE => '文件写入失败',
                UPLOAD_ERR_EXTENSION => 'PHP扩展阻止了文件上传'
            ];
            throw new Exception($error_messages[$file['error']] ?? '未知上传错误');
        }

        // 1. 文件大小校验
if($file['size'] > $this->max_file_size) {
            throw new Exception('文件大小不能超过 ' . ($this->max_file_size / 1024 / 1024) . 'MB');
        }

        // 2. 扩展名白名单校验
$original_name = basename($file['name']); // 使用basename防止路径遍历
$file_ext = strtolower(pathinfo($original_name, PATHINFO_EXTENSION));

        if(!in_array($file_ext, $this->allowed_extensions)) {
            throw new Exception('不允许的文件类型.仅支持: ' . implode(', ', $this->allowed_extensions));
        }

        // 3. MIME类型校验(不可单独依赖)
        $detected_mime = mime_content_type($file['tmp_name']);
        if(!in_array($detected_mime, $this->allowed_mime_types)) {
            throw new Exception('检测到非法的MIME类型: ' . $detected_mime);
        }

        // 4. 文件头(魔术字节)校验
if(!$this->validateFileSignature($file['tmp_name'], $file_ext)) {
            throw new Exception('文件内容与扩展名不匹配,可能被篡改');
        }

        // 5. 图片文件二次校验
if(in_array($file_ext, ['jpg', 'jpeg', 'png', 'gif'])) {
            if(!$this->validateImageFile($file['tmp_name'])) {
                throw new Exception('图片文件损坏或包含恶意内容');
            }
        }

        // 6. 生成安全的随机文件名
$safe_filename = $this->generateSafeFilename($file_ext);
        $target_path = $this->upload_dir . $safe_filename;

        // 7. 确保上传目录存在且安全
$this->ensureSecureUploadDir();

        // 8. 移动文件并设置权限
if(!move_uploaded_file($file['tmp_name'], $target_path)) {
            throw new Exception('文件保存失败');
        }

        // 9. 设置安全文件权限
chmod($target_path, 0644);

        return [
            'original_name' => $original_name,
            'saved_name' => $safe_filename,
            'file_path' => $target_path,
            'file_size' => $file['size'],
            'file_type' => $detected_mime
        ];
    }

    /**
     * 验证文件魔术字节签名
*/
    private function validateFileSignature($tmp_file_path, $expected_ext) {
        if(!file_exists($tmp_file_path)) {
            return false;
        }

        $handle = fopen($tmp_file_path, 'rb');
        if(!$handle) {
            return false;
        }

        // 根据扩展名检查对应的魔术字节
$signature_length = strlen($this->image_signatures[$expected_ext] ?? '');
        $file_signature = fread($handle, $signature_length);
        fclose($handle);

        return $file_signature === ($this->image_signatures[$expected_ext] ?? '');
    }

    /**
     * 验证图片文件
*/
    private function validateImageFile($tmp_file_path) {
        // 使用getimagesize验证图片有效性
$image_info = @getimagesize($tmp_file_path);
        if($image_info === false) {
            return false;
        }

        // 检查图片是否包含PHP标签(图片马检测简化版)
        $file_content = file_get_contents($tmp_file_path);
        if(strpos($file_content, '<?php') !== false || strpos($file_content, '<?=') !== false) {
            return false;
        }

        return true;
    }

    /**
     * 生成安全的随机文件名
*/
    private function generateSafeFilename($extension) {
        // 使用更安全的随机生成方式
$random_bytes = random_bytes(16);
        $safe_name = bin2hex($random_bytes) . '.' . $extension;
        return $safe_name;
    }

    /**
     * 确保上传目录安全
*/
    private function ensureSecureUploadDir() {
        if(!is_dir($this->upload_dir)) {
            mkdir($this->upload_dir, 0755, true);
        }

        // 在目录中放置.htaccess文件防止直接执行PHP(Apache服务器)
        $htaccess_content = <<<HTACCESS
# 防止直接执行PHP文件
<Files *.php>
    Order Deny,Allow
    Deny from all
</Files>

# 防止目录列表
Options -Indexes

# 设置文件缓存头(针对图片)
<FilesMatch "\.(jpg|jpeg|png|gif)$">
    Header set Cache-Control "max-age=604800, public"
</FilesMatch>
HTACCESS;

        $htaccess_path = $this->upload_dir . '.htaccess';
        if(!file_exists($htaccess_path)) {
            file_put_contents($htaccess_path, $htaccess_content);
        }

        // 放置一个空白的index.html防止目录遍历
$index_path = $this->upload_dir . 'index.html';
        if(!file_exists($index_path)) {
            file_put_contents($index_path, '<html><body><!-- 目录访问被阻止 --></body></html>');
        }
    }
}

// 使用示例
try {
    $uploader = new SecureFileUploader();
    $result = $uploader->upload('userfile');

    echo "文件上传成功!<br>";
    echo "原始文件名: " . htmlspecialchars($result['original_name']) . "<br>";
    echo "保存文件名: " . htmlspecialchars($result['saved_name']) . "<br>";
    echo "文件大小: " . round($result['file_size'] / 1024, 2) . " KB<br>";

    // 显示图片预览
if(strpos($result['file_type'], 'image/') === 0) {
        echo "<img src='{$result['file_path']}' style='max-width: 400px; border: 1px solid #ddd;'>";
    }

} catch (Exception $e) {
    echo "上传失败: " . htmlspecialchars($e->getMessage());
}
?>

示例 4:防止目录遍历攻击的文件下载代码

<?php
// 安全的文件下载实现 - 防止目录遍历攻击
class SecureFileDownload {
    private $base_dir; // 允许访问的基准目录
private $allowed_extensions = ['pdf', 'txt', 'jpg', 'png', 'docx'];

    public function __construct($base_directory) {
        // 规范化基准目录路径
$this->base_dir = realpath($base_directory);
        if($this->base_dir === false) {
            throw new Exception('基准目录不存在: ' . $base_directory);
        }
    }

    /**
     * 安全地提供文件下载
*/
    public function downloadFile($requested_file) {
        // 1. 只允许文件名,不允许路径
$file_name = basename($requested_file);

        // 2. 验证扩展名
$file_ext = strtolower(pathinfo($file_name, PATHINFO_EXTENSION));
        if(!in_array($file_ext, $this->allowed_extensions)) {
            http_response_code(403);
            die('不允许的文件类型');
        }

        // 3. 构建完整路径
$file_path = $this->base_dir . DIRECTORY_SEPARATOR . $file_name;

        // 4. 规范化并验证路径(防止目录遍历)
        $real_path = realpath($file_path);
        if($real_path === false || strpos($real_path, $this->base_dir) !== 0) {
            // 文件不存在或路径不在基准目录内
http_response_code(404);
            die('文件不存在');
        }

        // 5. 验证确实是文件(不是目录)
        if(!is_file($real_path)) {
            http_response_code(403);
            die('拒绝访问');
        }

        // 6. 设置下载头
header('Content-Description: File Transfer');
        header('Content-Type: application/octet-stream');
        header('Content-Disposition: attachment; filename="' . rawurlencode($file_name) . '"');
        header('Content-Transfer-Encoding: binary');
        header('Expires: 0');
        header('Cache-Control: must-revalidate');
        header('Pragma: public');
        header('Content-Length: ' . filesize($real_path));

        // 7. 清空输出缓冲区并发送文件
ob_clean();
        flush();
        readfile($real_path);
        exit;
    }

    /**
     * 安全的文件查看(仅限图片)
     */
    public function viewImage($requested_file) {
        $file_name = basename($requested_file);
        $allowed_image_ext = ['jpg', 'jpeg', 'png', 'gif', 'webp'];

        $file_ext = strtolower(pathinfo($file_name, PATHINFO_EXTENSION));
        if(!in_array($file_ext, $allowed_image_ext)) {
            http_response_code(403);
            die('只允许查看图片文件');
        }

        $file_path = $this->base_dir . DIRECTORY_SEPARATOR . $file_name;
        $real_path = realpath($file_path);

        if($real_path === false || strpos($real_path, $this->base_dir) !== 0) {
            http_response_code(404);
            die('图片不存在');
        }

        if(!is_file($real_path)) {
            http_response_code(403);
            die('拒绝访问');
        }

        // 根据扩展名设置正确的Content-Type
        $mime_types = [
            'jpg' => 'image/jpeg',
            'jpeg' => 'image/jpeg',
            'png' => 'image/png',
            'gif' => 'image/gif',
            'webp' => 'image/webp'
        ];

        header('Content-Type: ' . ($mime_types[$file_ext] ?? 'image/jpeg'));
        header('Content-Length: ' . filesize($real_path));

        readfile($real_path);
        exit;
    }
}

// 使用示例
try {
    // 假设我们的文件存储在files/目录下
$downloader = new SecureFileDownload(__DIR__ . '/files');

    // 从URL参数获取请求的文件名
$requested_file = $_GET['file'] ?? '';

    if(empty($requested_file)) {
        die('请指定要下载的文件名');
    }

    // 根据参数决定是下载还是查看
$action = $_GET['action'] ?? 'download';

    if($action === 'view') {
        $downloader->viewImage($requested_file);
    } else {
        $downloader->downloadFile($requested_file);
    }

} catch (Exception $e) {
    http_response_code(500);
    echo '错误: ' . htmlspecialchars($e->getMessage());
}
?>

示例 5:文件上传的 HTML 表单与客户端校验

<!DOCTYPE html>
<html lang="zh-CN">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>安全的文件上传表单</title>
    <style>
      .upload-container {
        max-width: 500px;
        margin: 50px auto;
        padding: 20px;
        border: 1px solid #ddd;
        border-radius: 5px;
        background-color: #f9f9f9;
      }
      .form-group {
        margin-bottom: 15px;
      }
      label {
        display: block;
        margin-bottom: 5px;
        font-weight: bold;
      }
      input[type="file"] {
        width: 100%;
        padding: 8px;
        border: 1px solid #ddd;
        border-radius: 3px;
      }
      button {
        background-color: #4caf50;
        color: white;
        padding: 10px 20px;
        border: none;
        border-radius: 3px;
        cursor: pointer;
        font-size: 16px;
      }
      button:hover {
        background-color: #45a049;
      }
      .error {
        color: #d9534f;
        margin-top: 5px;
        font-size: 14px;
      }
      .progress-container {
        display: none;
        margin-top: 15px;
      }
      .progress-bar {
        width: 100%;
        height: 20px;
        background-color: #f0f0f0;
        border-radius: 3px;
        overflow: hidden;
      }
      .progress-fill {
        height: 100%;
        background-color: #4caf50;
        width: 0%;
        transition: width 0.3s;
      }
    </style>
  </head>
  <body>
    <div class="upload-container">
      <h2>安全文件上传演示</h2>
      <p>仅允许上传JPG、PNG、GIF格式的图片文件,大小不超过2MB.</p>

      <form
        id="uploadForm"
        action="secure_upload.php"
        method="POST"
        enctype="multipart/form-data"
      >
        <div class="form-group">
          <label for="userfile">选择文件:</label>
          <input
            type="file"
            name="userfile"
            id="userfile"
            accept=".jpg,.jpeg,.png,.gif"
            required
          />
          <div id="fileError" class="error"></div>
        </div>

        <div class="form-group">
          <label for="description">文件描述(可选):</label>
          <input
            type="text"
            name="description"
            id="description"
            maxlength="100"
          />
        </div>

        <div class="progress-container" id="progressContainer">
          <div class="progress-bar">
            <div class="progress-fill" id="progressFill"></div>
          </div>
          <div id="progressText">上传中: 0%</div>
        </div>

        <button type="submit" id="uploadButton">上传文件</button>
      </form>

      <div id="result" style="margin-top: 20px;"></div>
    </div>

    <script>
      document.addEventListener("DOMContentLoaded", function () {
        const form = document.getElementById("uploadForm");
        const fileInput = document.getElementById("userfile");
        const fileError = document.getElementById("fileError");
        const uploadButton = document.getElementById("uploadButton");
        const progressContainer = document.getElementById("progressContainer");
        const progressFill = document.getElementById("progressFill");
        const progressText = document.getElementById("progressText");
        const resultDiv = document.getElementById("result");

        // 最大文件大小(2MB)
        const MAX_FILE_SIZE = 2 * 1024 * 1024;

        // 允许的文件类型
        const ALLOWED_TYPES = ["image/jpeg", "image/png", "image/gif"];

        // 客户端文件验证
        fileInput.addEventListener("change", function () {
          const file = this.files[0];
          fileError.textContent = "";

          if (!file) {
            return;
          }

          // 1. 验证文件大小
          if (file.size > MAX_FILE_SIZE) {
            fileError.textContent = "文件大小不能超过2MB";
            this.value = ""; // 清空文件选择
            return;
          }

          // 2. 验证文件类型
          if (!ALLOWED_TYPES.includes(file.type)) {
            fileError.textContent = "只允许JPG、PNG、GIF格式的图片文件";
            this.value = "";
            return;
          }

          // 3. 验证扩展名(双重检查)
          const fileName = file.name.toLowerCase();
          const validExtensions = [".jpg", ".jpeg", ".png", ".gif"];
          const hasValidExtension = validExtensions.some((ext) =>
            fileName.endsWith(ext)
          );

          if (!hasValidExtension) {
            fileError.textContent = "文件扩展名不被允许";
            this.value = "";
            return;
          }

          // 验证通过,可以显示文件信息
          console.log("文件验证通过:", {
            name: file.name,
            size: (file.size / 1024 / 1024).toFixed(2) + " MB",
            type: file.type,
          });
        });

        // AJAX文件上传(可选增强功能)
        form.addEventListener("submit", function (e) {
          e.preventDefault();

          const file = fileInput.files[0];
          if (!file) {
            fileError.textContent = "请选择文件";
            return;
          }

          // 禁用上传按钮,防止重复提交
          uploadButton.disabled = true;
          uploadButton.textContent = "上传中...";

          // 显示进度条
          progressContainer.style.display = "block";

          // 使用FormData对象
          const formData = new FormData(this);

          // 使用XMLHttpRequest以便获取上传进度
          const xhr = new XMLHttpRequest();

          // 上传进度事件
          xhr.upload.addEventListener("progress", function (e) {
            if (e.lengthComputable) {
              const percentComplete = Math.round((e.loaded / e.total) * 100);
              progressFill.style.width = percentComplete + "%";
              progressText.textContent = "上传中: " + percentComplete + "%";
            }
          });

          // 请求完成
          xhr.addEventListener("load", function () {
            progressContainer.style.display = "none";
            uploadButton.disabled = false;
            uploadButton.textContent = "上传文件";

            if (xhr.status === 200) {
              try {
                const response = JSON.parse(xhr.responseText);
                if (response.success) {
                  resultDiv.innerHTML =
                    '<div style="color: green; padding: 10px; background-color: #dff0d8; border: 1px solid #d6e9c6; border-radius: 3px;">' +
                    "<strong>上传成功!</strong><br>" +
                    "文件名: " +
                    response.filename +
                    "<br>" +
                    "文件大小: " +
                    response.size +
                    " KB<br>" +
                    (response.preview
                      ? '<img src="' +
                        response.preview +
                        '" style="max-width: 100%; margin-top: 10px;">'
                      : "") +
                    "</div>";

                  // 重置表单
                  form.reset();
                } else {
                  resultDiv.innerHTML =
                    '<div style="color: #d9534f; padding: 10px; background-color: #f2dede; border: 1px solid #ebccd1; border-radius: 3px;">' +
                    "<strong>上传失败:</strong> " +
                    response.message +
                    "</div>";
                }
              } catch (e) {
                resultDiv.innerHTML =
                  '<div style="color: #d9534f; padding: 10px; background-color: #f2dede; border: 1px solid #ebccd1; border-radius: 3px;">' +
                  "<strong>服务器响应解析失败</strong>" +
                  "</div>";
              }
            } else {
              resultDiv.innerHTML =
                '<div style="color: #d9534f; padding: 10px; background-color: #f2dede; border: 1px solid #ebccd1; border-radius: 3px;">' +
                "<strong>服务器错误:</strong> HTTP " +
                xhr.status +
                "</div>";
            }
          });

          // 请求错误
          xhr.addEventListener("error", function () {
            progressContainer.style.display = "none";
            uploadButton.disabled = false;
            uploadButton.textContent = "上传文件";

            resultDiv.innerHTML =
              '<div style="color: #d9534f; padding: 10px; background-color: #f2dede; border: 1px solid #ebccd1; border-radius: 3px;">' +
              "<strong>网络错误,请稍后重试</strong>" +
              "</div>";
          });

          // 发送请求
          xhr.open("POST", form.action);
          xhr.send(formData);
        });

        // 传统表单提交方式(备用)
        const traditionalSubmitBtn = document.createElement("button");
        traditionalSubmitBtn.type = "submit";
        traditionalSubmitBtn.textContent = "传统方式上传";
        traditionalSubmitBtn.style.marginLeft = "10px";
        traditionalSubmitBtn.style.backgroundColor = "#337ab7";

        traditionalSubmitBtn.addEventListener("click", function (e) {
          // 允许表单默认提交行为
          form.removeEventListener("submit", arguments.callee);
        });

        // 将传统提交按钮添加到表单中
        form.appendChild(traditionalSubmitBtn);
      });
    </script>
  </body>
</html>

预期输出:
当用户选择文件后,客户端 JavaScript 会立即验证文件大小和类型.如果选择了一个 3MB 的文件,会立即显示"文件大小不能超过 2MB"的错误信息.如果选择了一个 PDF 文件,会显示"只允许 JPG、PNG、GIF 格式的图片文件".只有通过验证的文件才能被提交.

实战项目:构建安全图片相册系统

项目需求分析

我们将构建一个完整的图片相册系统,包含以下功能:

  1. 用户认证系统:用户注册、登录、会话管理
  2. 安全图片上传:实现多层安全校验的图片上传功能
  3. 图片管理:查看、删除用户自己的图片
  4. 相册分享:生成安全的分享链接
  5. 管理员功能:管理所有用户和图片(可选)

技术方案

  1. 前端:HTML5 + CSS3 + JavaScript(客户端校验)
  2. 后端:PHP 7.4+(服务器端处理)
  3. 数据库:MySQL(存储用户信息和图片元数据)
  4. 文件存储:本地文件系统(存储上传的图片)
  5. 安全措施:
    • 图片文件多层校验
  • 防止 SQL 注入(使用 PDO 预处理语句)
    • 防止 XSS 攻击(输出转义)
    • 防止 CSRF 攻击(Token 验证)
    • 安全的会话管理

项目结构

secure_gallery/
├── index.php              # 首页
├── login.php             # 登录页面
├── register.php          # 注册页面
├── logout.php            # 退出登录
├── upload.php            # 文件上传处理
├── gallery.php           # 个人相册
├── share.php             # 分享查看
├── admin/               # 管理后台
│   ├── index.php
│   ├── users.php
│   └── images.php
├── includes/             # 包含文件
│   ├── config.php       # 配置文件
│   ├── database.php     # 数据库连接
│   ├── auth.php         # 认证函数
│   ├── uploader.php     # 文件上传类
│   └── functions.php    # 通用函数
├── uploads/              # 上传文件存储目录
│   ├── images/          # 图片文件(Web根目录外)
│   └── thumbs/          # 缩略图
└── assets/              # 静态资源
├── css/
    ├── js/
    └── images/

分步骤实现

步骤 1:数据库设计与初始化
-- 创建数据库
CREATE DATABASE IF NOT EXISTS secure_gallery CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
USE secure_gallery;

-- 用户表
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,
    full_name VARCHAR(100),
    role ENUM('user', 'admin') DEFAULT 'user',
    status ENUM('active', 'inactive', 'suspended') DEFAULT 'active',
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
    last_login TIMESTAMP NULL,
    INDEX idx_username (username),
    INDEX idx_email (email)
);

-- 图片表
CREATE TABLE images (
    id INT PRIMARY KEY AUTO_INCREMENT,
    user_id INT NOT NULL,
    original_name VARCHAR(255) NOT NULL,
    stored_name VARCHAR(255) UNIQUE NOT NULL,
    file_path VARCHAR(500) NOT NULL,
    file_size INT NOT NULL,
    mime_type VARCHAR(50) NOT NULL,
    width INT,
    height INT,
    title VARCHAR(200),
    description TEXT,
    is_public BOOLEAN DEFAULT FALSE,
    share_token CHAR(32) UNIQUE,
    view_count INT DEFAULT 0,
    uploaded_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE,
    INDEX idx_user_id (user_id),
    INDEX idx_share_token (share_token),
    INDEX idx_uploaded_at (uploaded_at)
);

-- 创建管理员用户(密码:Admin@123)
INSERT INTO users (username, email, password_hash, full_name, role) VALUES
('admin', 'admin@example.com', '$2y$10$92IXUNpkjO0rOQ5byMi.Ye4oKoEa3Ro9llC/.og/at2.uheWG/igi', '系统管理员', 'admin');

-- 创建测试用户(密码:Test@123)
INSERT INTO users (username, email, password_hash, full_name) VALUES
('testuser', 'user@example.com', '$2y$10$92IXUNpkjO0rOQ5byMi.Ye4oKoEa3Ro9llC/.og/at2.uheWG/igi', '测试用户');
步骤 2:配置文件与数据库连接
<?php
// includes/config.php
// 安全图片相册系统 - 配置文件
// 错误报告设置(开发环境)
error_reporting(E_ALL);
ini_set('display_errors', 1);

// 生产环境应设置为:
// error_reporting(0);
// ini_set('display_errors', 0);
// ini_set('log_errors', 1);
// ini_set('error_log', '/path/to/php-error.log');

// 时区设置
date_default_timezone_set('Asia/Shanghai');

// 会话安全设置
ini_set('session.cookie_httponly', 1);
ini_set('session.cookie_secure', 1); // 仅HTTPS启用
ini_set('session.use_only_cookies', 1);
ini_set('session.cookie_samesite', 'Strict');

// 应用配置
define('APP_NAME', '安全图片相册');
define('APP_VERSION', '1.0.0');
define('BASE_URL', 'http:// localhost/secure_gallery'); // 根据实际修改
// 数据库配置
define('DB_HOST', 'localhost');
define('DB_NAME', 'secure_gallery');
define('DB_USER', 'root'); // 根据实际修改
define('DB_PASS', ''); // 根据实际修改
define('DB_CHARSET', 'utf8mb4');

// 文件上传配置
define('UPLOAD_MAX_SIZE', 5 * 1024 * 1024); // 5MB
define('ALLOWED_IMAGE_TYPES', ['image/jpeg', 'image/png', 'image/gif', 'image/webp']);
define('ALLOWED_EXTENSIONS', ['jpg', 'jpeg', 'png', 'gif', 'webp']);

// 文件存储路径(建议放在Web根目录外)
define('UPLOAD_BASE_DIR', dirname(__DIR__) . '/private_uploads/');
define('THUMBNAIL_DIR', 'thumbs/');

// 安全配置
define('CSRF_TOKEN_NAME', 'csrf_token');
define('SESSION_TIMEOUT', 1800); // 30分钟
// 管理员邮箱
define('ADMIN_EMAIL', 'admin@example.com');

// 自动加载类
spl_autoload_register(function ($class_name) {
    $file = __DIR__ . '/../classes/' . $class_name . '.php';
    if (file_exists($file)) {
        require_once $file;
    }
});

// 启动会话(放在配置加载后)
if (session_status() === PHP_SESSION_NONE) {
    session_start();

    // 会话固定防护:定期更新会话ID
    if (!isset($_SESSION['created'])) {
        $_SESSION['created'] = time();
    } else if (time() - $_SESSION['created'] > 600) { // 每10分钟更新一次
session_regenerate_id(true);
        $_SESSION['created'] = time();
    }
}

// 设置CSRF Token(如果不存在)
if (empty($_SESSION[CSRF_TOKEN_NAME])) {
    $_SESSION[CSRF_TOKEN_NAME] = bin2hex(random_bytes(32));
}

// 通用函数:生成CSRF Token字段
function csrf_field() {
    return '<input type="hidden" name="' . CSRF_TOKEN_NAME . '" value="' . $_SESSION[CSRF_TOKEN_NAME] . '">';
}

// 通用函数:验证CSRF Token
function validate_csrf_token($token) {
    return isset($_SESSION[CSRF_TOKEN_NAME]) && hash_equals($_SESSION[CSRF_TOKEN_NAME], $token);
}

// 通用函数:安全的跳转
function redirect($url, $permanent = false) {
    if ($permanent) {
        header('HTTP/1.1 301 Moved Permanently');
    }
    header('Location: ' . $url);
    exit();
}

// 通用函数:安全的输出
function escape($string) {
    return htmlspecialchars($string, ENT_QUOTES, 'UTF-8');
}
?>
<?php
// includes/database.php
// 数据库连接类(使用PDO,防止SQL注入)

class Database {
    private static $instance = null;
    private $connection;

    private function __construct() {
        try {
            $dsn = 'mysql:host=' . DB_HOST . ';dbname=' . DB_NAME . ';charset=' . DB_CHARSET;
            $options = [
                PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
                PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
                PDO::ATTR_EMULATE_PREPARES => false, // 禁用预处理模拟,提高安全性
PDO::ATTR_STRINGIFY_FETCHES => false
            ];

            $this->connection = new PDO($dsn, DB_USER, DB_PASS, $options);
        } catch (PDOException $e) {
            // 生产环境应记录到日志文件,而不是直接显示
error_log('数据库连接失败: ' . $e->getMessage());
            die('数据库连接失败,请稍后重试');
        }
    }

    public static function getInstance() {
        if (self::$instance === null) {
            self::$instance = new Database();
        }
        return self::$instance;
    }

    public function getConnection() {
        return $this->connection;
    }

    /**
     * 安全的查询执行(预处理语句)
     */
    public function query($sql, $params = []) {
        try {
            $stmt = $this->connection->prepare($sql);
            $stmt->execute($params);
            return $stmt;
        } catch (PDOException $e) {
            error_log('数据库查询错误: ' . $e->getMessage() . ' SQL: ' . $sql);
            throw new Exception('数据库操作失败');
        }
    }

    /**
     * 获取单行结果
*/
    public function fetch($sql, $params = []) {
        $stmt = $this->query($sql, $params);
        return $stmt->fetch();
    }

    /**
     * 获取所有结果
*/
    public function fetchAll($sql, $params = []) {
        $stmt = $this->query($sql, $params);
        return $stmt->fetchAll();
    }

    /**
     * 插入数据并返回最后插入的ID
     */
    public function insert($sql, $params = []) {
        $this->query($sql, $params);
        return $this->connection->lastInsertId();
    }

    /**
     * 开始事务
*/
    public function beginTransaction() {
        return $this->connection->beginTransaction();
    }

    /**
     * 提交事务
*/
    public function commit() {
        return $this->connection->commit();
    }

    /**
     * 回滚事务
*/
    public function rollBack() {
        return $this->connection->rollBack();
    }
}
?>
步骤 3:增强版安全文件上传类
<?php
// includes/uploader.php
// 安全文件上传类 - 完整的多层防御实现
class SecureUploader {
    private $db;
    private $allowed_mime_types;
    private $allowed_extensions;
    private $max_file_size;
    private $upload_base_dir;

    public function __construct() {
        $this->db = Database::getInstance()->getConnection();
        $this->allowed_mime_types = ALLOWED_IMAGE_TYPES;
        $this->allowed_extensions = ALLOWED_EXTENSIONS;
        $this->max_file_size = UPLOAD_MAX_SIZE;
        $this->upload_base_dir = UPLOAD_BASE_DIR;

        // 确保上传目录存在且安全
$this->ensureSecureDirectories();
    }

    /**
     * 处理文件上传
*/
    public function upload($file_field, $user_id, $title = '', $description = '', $is_public = false) {
        // 验证上传状态
if (!isset($_FILES[$file_field]) || $_FILES[$file_field]['error'] !== UPLOAD_ERR_OK) {
            throw new Exception($this->getUploadErrorMessage($_FILES[$file_field]['error'] ?? UPLOAD_ERR_NO_FILE));
        }

        $file = $_FILES[$file_field];

        // 1. 文件大小校验
if ($file['size'] > $this->max_file_size) {
            throw new Exception('文件大小不能超过 ' . ($this->max_file_size / 1024 / 1024) . 'MB');
        }

        // 2. 扩展名白名单校验
$original_name = basename($file['name']);
        $file_ext = strtolower(pathinfo($original_name, PATHINFO_EXTENSION));

        if (!in_array($file_ext, $this->allowed_extensions)) {
            throw new Exception('不支持的文件类型.仅允许: ' . implode(', ', $this->allowed_extensions));
        }

        // 3. MIME类型校验
$detected_mime = mime_content_type($file['tmp_name']);
        if (!in_array($detected_mime, $this->allowed_mime_types)) {
            throw new Exception('检测到非法的MIME类型: ' . $detected_mime);
        }

        // 4. 文件头(魔术字节)校验
if (!$this->validateFileSignature($file['tmp_name'], $file_ext)) {
            throw new Exception('文件内容与扩展名不匹配,可能被篡改');
        }

        // 5. 图片文件深度校验
if (!$this->validateImageFile($file['tmp_name'])) {
            throw new Exception('图片文件损坏或包含恶意内容');
        }

        // 6. 检查图片中是否包含Webshell代码(简化版)
        if ($this->containsMaliciousContent($file['tmp_name'])) {
            throw new Exception('检测到潜在的安全威胁,文件被拒绝');
        }

        // 7. 生成安全的随机文件名
$stored_name = $this->generateSafeFilename($file_ext);
        $file_path = $this->upload_base_dir . 'images/' . $stored_name;

        // 8. 移动文件
if (!move_uploaded_file($file['tmp_name'], $file_path)) {
            throw new Exception('文件保存失败,请检查目录权限');
        }

        // 9. 设置安全权限
chmod($file_path, 0644);

        // 10. 获取图片尺寸
$image_info = getimagesize($file_path);
        $width = $image_info[0] ?? 0;
        $height = $image_info[1] ?? 0;

        // 11. 生成缩略图
$thumbnail_path = $this->generateThumbnail($file_path, $stored_name, $width, $height);

        // 12. 生成分享令牌(如果图片是公开的)
        $share_token = $is_public ? $this->generateShareToken() : null;

        // 13. 保存到数据库
$image_id = $this->saveImageToDatabase(
            $user_id,
            $original_name,
            $stored_name,
            $file_path,
            $file['size'],
            $detected_mime,
            $width,
            $height,
            $title,
            $description,
            $is_public,
            $share_token
        );

        // 14. 记录安全日志
$this->logSecurityEvent('file_upload', [
            'user_id' => $user_id,
            'image_id' => $image_id,
            'original_name' => $original_name,
            'file_size' => $file['size'],
            'ip_address' => $_SERVER['REMOTE_ADDR'] ?? 'unknown'
        ]);

        return [
            'id' => $image_id,
            'original_name' => $original_name,
            'stored_name' => $stored_name,
            'file_path' => $file_path,
            'thumbnail_path' => $thumbnail_path,
            'share_token' => $share_token,
            'width' => $width,
            'height' => $height
        ];
    }

    /**
     * 验证文件魔术字节签名
*/
    private function validateFileSignature($tmp_file_path, $expected_ext) {
        $signatures = [
            'jpg' => "\xFF\xD8\xFF",
            'jpeg' => "\xFF\xD8\xFF",
            'png' => "\x89\x50\x4E\x47\x0D\x0A\x1A\x0A",
            'gif' => "GIF89a",
            'webp' => "RIFF"
        ];

        if (!isset($signatures[$expected_ext])) {
            return false;
        }

        $expected_signature = $signatures[$expected_ext];
        $signature_length = strlen($expected_signature);

        $handle = fopen($tmp_file_path, 'rb');
        if (!$handle) {
            return false;
        }

        $file_signature = fread($handle, $signature_length);
        fclose($handle);

        return strncmp($file_signature, $expected_signature, $signature_length) === 0;
    }

    /**
     * 验证图片文件
*/
    private function validateImageFile($tmp_file_path) {
        // 使用GD库验证图片
$image_info = @getimagesize($tmp_file_path);
        if ($image_info === false) {
            return false;
        }

        // 验证图片是否可以成功加载
$image_type = $image_info[2];
        $supported_types = [IMAGETYPE_JPEG, IMAGETYPE_PNG, IMAGETYPE_GIF, IMAGETYPE_WEBP];

        if (!in_array($image_type, $supported_types)) {
            return false;
        }

        return true;
    }

    /**
     * 检查文件是否包含恶意内容
*/
    private function containsMaliciousContent($file_path) {
        $content = file_get_contents($file_path);

        // 检查常见的Webshell特征(简化示例)
        $dangerous_patterns = [
            '/<\?php\s*(system|exec|shell_exec|passthru|eval|assert)/i',
            '/<script[^>]*>.*?(eval|document\.write|document\.cookie).*?<\/script>/is',
            '/onload\s*=|onerror\s*=|onclick\s*=/i',
            '/javascript:/i'
        ];

        foreach ($dangerous_patterns as $pattern) {
            if (preg_match($pattern, $content)) {
                return true;
            }
        }

        // 检查是否包含PHP标签(对于图片文件不应该有)
        if (preg_match('/<\?php|<%|<\?=/i', $content)) {
            return true;
        }

        return false;
    }

    /**
     * 生成安全的随机文件名
*/
    private function generateSafeFilename($extension) {
        // 使用密码学安全的随机数生成器
$random_bytes = random_bytes(16);
        $safe_name = bin2hex($random_bytes) . '.' . $extension;
        return $safe_name;
    }

    /**
     * 生成分享令牌
*/
    private function generateShareToken() {
        return bin2hex(random_bytes(16));
    }

    /**
     * 生成缩略图
*/
    private function generateThumbnail($source_path, $stored_name, $width, $height) {
        $thumb_dir = $this->upload_base_dir . THUMBNAIL_DIR;
        $thumb_path = $thumb_dir . $stored_name;

        // 创建缩略图目录
if (!is_dir($thumb_dir)) {
            mkdir($thumb_dir, 0755, true);
        }

        // 最大缩略图尺寸
$max_width = 200;
        $max_height = 200;

        // 计算缩略图尺寸
if ($width > $height) {
            $new_width = $max_width;
            $new_height = intval($height * ($max_width / $width));
        } else {
            $new_height = $max_height;
            $new_width = intval($width * ($max_height / $height));
        }

        // 创建缩略图
$source_image = imagecreatefromstring(file_get_contents($source_path));
        $thumbnail = imagecreatetruecolor($new_width, $new_height);

        // 保持透明度(针对PNG和GIF)
        imagealphablending($thumbnail, false);
        imagesavealpha($thumbnail, true);

        imagecopyresampled($thumbnail, $source_image, 0, 0, 0, 0,
            $new_width, $new_height, $width, $height);

        // 保存缩略图
$extension = strtolower(pathinfo($stored_name, PATHINFO_EXTENSION));
        switch ($extension) {
            case 'jpg':
            case 'jpeg':
                imagejpeg($thumbnail, $thumb_path, 85);
                break;
            case 'png':
                imagepng($thumbnail, $thumb_path, 8);
                break;
            case 'gif':
                imagegif($thumbnail, $thumb_path);
                break;
            case 'webp':
                imagewebp($thumbnail, $thumb_path, 85);
                break;
        }

        imagedestroy($source_image);
        imagedestroy($thumbnail);

        return $thumb_path;
    }

    /**
     * 保存图片信息到数据库
*/
    private function saveImageToDatabase($user_id, $original_name, $stored_name, $file_path,
                                        $file_size, $mime_type, $width, $height,
                                        $title, $description, $is_public, $share_token) {
        try {
            $sql = "INSERT INTO images (user_id, original_name, stored_name, file_path,
                    file_size, mime_type, width, height, title, description, is_public, share_token)
                    VALUES (:user_id, :original_name, :stored_name, :file_path,
                    :file_size, :mime_type, :width, :height, :title, :description, :is_public, :share_token)";

            $stmt = $this->db->prepare($sql);
            $stmt->execute([
                ':user_id' => $user_id,
                ':original_name' => $original_name,
                ':stored_name' => $stored_name,
                ':file_path' => $file_path,
                ':file_size' => $file_size,
                ':mime_type' => $mime_type,
                ':width' => $width,
                ':height' => $height,
                ':title' => $title,
                ':description' => $description,
                ':is_public' => $is_public ? 1 : 0,
                ':share_token' => $share_token
            ]);

            return $this->db->lastInsertId();
        } catch (PDOException $e) {
            // 如果数据库保存失败,删除已上传的文件
if (file_exists($file_path)) {
                unlink($file_path);
            }
            throw new Exception('图片信息保存失败: ' . $e->getMessage());
        }
    }

    /**
     * 确保上传目录安全
*/
    private function ensureSecureDirectories() {
        $directories = [
            $this->upload_base_dir,
            $this->upload_base_dir . 'images/',
            $this->upload_base_dir . THUMBNAIL_DIR
        ];

        foreach ($directories as $dir) {
            if (!is_dir($dir)) {
                mkdir($dir, 0755, true);
            }

            // 在目录中放置.htaccess(Apache)或web.config(IIS)防止直接执行
$this->createSecurityFile($dir);

            // 放置空白的index.html防止目录列表
$index_file = $dir . 'index.html';
            if (!file_exists($index_file)) {
                file_put_contents($index_file, '<!DOCTYPE html><html><body></body></html>');
            }
        }
    }

    /**
     * 创建安全配置文件
*/
    private function createSecurityFile($directory) {
        // Apache服务器
$htaccess_content = <<<HTACCESS
# 防止直接执行脚本文件
<FilesMatch "\.(php|php5|phtml|pl|py|jsp|asp|sh|cgi)$">
    Order Deny,Allow
    Deny from all
</FilesMatch>

# 防止目录列表
Options -Indexes

# 设置缓存头
<FilesMatch "\.(jpg|jpeg|png|gif|webp)$">
    Header set Cache-Control "max-age=2592000, public"
</FilesMatch>

# 限制文件访问(仅允许图片类型)
<FilesMatch "\.(jpg|jpeg|png|gif|webp)$">
    Allow from all
</FilesMatch>

<FilesMatch "\.(php|txt|sql|log)$">
    Deny from all
</FilesMatch>
HTACCESS;

        $htaccess_path = $directory . '.htaccess';
        if (!file_exists($htaccess_path)) {
            file_put_contents($htaccess_path, $htaccess_content);
        }
    }

    /**
     * 记录安全事件
*/
    private function logSecurityEvent($event_type, $data) {
        $log_entry = sprintf(
            "[%s] %s: %s\n",
            date('Y-m-d H:i:s'),
            $event_type,
            json_encode($data, JSON_UNESCAPED_UNICODE)
        );

        $log_file = $this->upload_base_dir . 'security.log';
        file_put_contents($log_file, $log_entry, FILE_APPEND | LOCK_EX);
    }

    /**
     * 获取上传错误信息
*/
    private function getUploadErrorMessage($error_code) {
        $errors = [
            UPLOAD_ERR_INI_SIZE => '文件大小超过服务器限制',
            UPLOAD_ERR_FORM_SIZE => '文件大小超过表单限制',
            UPLOAD_ERR_PARTIAL => '文件只有部分被上传',
            UPLOAD_ERR_NO_FILE => '没有文件被上传',
            UPLOAD_ERR_NO_TMP_DIR => '缺少临时文件夹',
            UPLOAD_ERR_CANT_WRITE => '文件写入失败',
            UPLOAD_ERR_EXTENSION => 'PHP扩展阻止了文件上传'
        ];

        return $errors[$error_code] ?? '未知上传错误';
    }

    /**
     * 获取用户图片列表
*/
    public function getUserImages($user_id, $limit = 20, $offset = 0) {
        $sql = "SELECT * FROM images WHERE user_id = :user_id
                ORDER BY uploaded_at DESC LIMIT :limit OFFSET :offset";

        $stmt = $this->db->prepare($sql);
        $stmt->bindValue(':user_id', $user_id, PDO::PARAM_INT);
        $stmt->bindValue(':limit', $limit, PDO::PARAM_INT);
        $stmt->bindValue(':offset', $offset, PDO::PARAM_INT);
        $stmt->execute();

        return $stmt->fetchAll();
    }

    /**
     * 通过分享令牌获取图片
*/
    public function getImageByToken($token) {
        $sql = "SELECT i.*, u.username FROM images i
                JOIN users u ON i.user_id = u.id
                WHERE i.share_token = :token AND i.is_public = 1";

        $stmt = $this->db->prepare($sql);
        $stmt->execute([':token' => $token]);
        $image = $stmt->fetch();

        if ($image) {
            // 更新查看次数
$update_sql = "UPDATE images SET view_count = view_count + 1 WHERE id = :id";
            $this->db->prepare($update_sql)->execute([':id' => $image['id']]);
        }

        return $image;
    }

    /**
     * 删除图片
*/
    public function deleteImage($image_id, $user_id) {
        // 获取图片信息
$sql = "SELECT * FROM images WHERE id = :id AND user_id = :user_id";
        $stmt = $this->db->prepare($sql);
        $stmt->execute([':id' => $image_id, ':user_id' => $user_id]);
        $image = $stmt->fetch();

        if (!$image) {
            throw new Exception('图片不存在或无权删除');
        }

        // 删除物理文件
if (file_exists($image['file_path'])) {
            unlink($image['file_path']);
        }

        // 删除缩略图
$thumb_path = $this->upload_base_dir . THUMBNAIL_DIR . $image['stored_name'];
        if (file_exists($thumb_path)) {
            unlink($thumb_path);
        }

        // 删除数据库记录
$delete_sql = "DELETE FROM images WHERE id = :id";
        $this->db->prepare($delete_sql)->execute([':id' => $image_id]);

        // 记录安全日志
$this->logSecurityEvent('file_delete', [
            'user_id' => $user_id,
            'image_id' => $image_id,
            'ip_address' => $_SERVER['REMOTE_ADDR'] ?? 'unknown'
        ]);

        return true;
    }
}
?>
步骤 4:用户认证与权限管理
<?php
// includes/auth.php
// 用户认证与权限管理
class Auth {
    private $db;

    public function __construct() {
        $this->db = Database::getInstance()->getConnection();
    }

    /**
     * 用户注册
*/
    public function register($username, $email, $password, $full_name = '') {
        // 验证输入
$this->validateRegistrationInput($username, $email, $password);

        // 检查用户名是否已存在
if ($this->usernameExists($username)) {
            throw new Exception('用户名已存在');
        }

        // 检查邮箱是否已存在
if ($this->emailExists($email)) {
            throw new Exception('邮箱地址已被注册');
        }

        // 创建密码哈希
$password_hash = password_hash($password, PASSWORD_DEFAULT);

        if ($password_hash === false) {
            throw new Exception('密码加密失败');
        }

        // 插入用户记录
$sql = "INSERT INTO users (username, email, password_hash, full_name)
                VALUES (:username, :email, :password_hash, :full_name)";

        try {
            $stmt = $this->db->prepare($sql);
            $stmt->execute([
                ':username' => $username,
                ':email' => $email,
                ':password_hash' => $password_hash,
                ':full_name' => $full_name
            ]);

            $user_id = $this->db->lastInsertId();

            // 记录安全日志
$this->logSecurityEvent('user_register', [
                'user_id' => $user_id,
                'username' => $username,
                'ip_address' => $_SERVER['REMOTE_ADDR'] ?? 'unknown'
            ]);

            return $user_id;
        } catch (PDOException $e) {
            throw new Exception('用户注册失败: ' . $e->getMessage());
        }
    }

    /**
     * 用户登录
*/
    public function login($username, $password) {
        // 防止暴力破解:检查失败次数
if ($this->isLoginBlocked($username)) {
            throw new Exception('账户暂时被锁定,请稍后重试');
        }

        // 获取用户信息
$user = $this->getUserByUsername($username);

        if (!$user) {
            // 记录失败尝试
$this->recordFailedLogin($username);
            throw new Exception('用户名或密码错误');
        }

        // 验证密码
if (!password_verify($password, $user['password_hash'])) {
            // 记录失败尝试
$this->recordFailedLogin($username);
            throw new Exception('用户名或密码错误');
        }

        // 检查账户状态
if ($user['status'] !== 'active') {
            throw new Exception('账户状态异常,请联系管理员');
        }

        // 清除失败记录
$this->clearFailedLogins($username);

        // 更新最后登录时间
$this->updateLastLogin($user['id']);

        // 设置会话
$this->setUserSession($user);

        // 记录安全日志
$this->logSecurityEvent('user_login', [
            'user_id' => $user['id'],
            'username' => $username,
            'ip_address' => $_SERVER['REMOTE_ADDR'] ?? 'unknown',
            'user_agent' => $_SERVER['HTTP_USER_AGENT'] ?? 'unknown'
        ]);

        return true;
    }

    /**
     * 设置用户会话
*/
    private function setUserSession($user) {
        // 销毁旧会话,创建新会话(防止会话固定)
        session_regenerate_id(true);

        $_SESSION['user_id'] = $user['id'];
        $_SESSION['username'] = $user['username'];
        $_SESSION['role'] = $user['role'];
        $_SESSION['login_time'] = time();

        // 设置会话过期时间
$_SESSION['expire_time'] = time() + SESSION_TIMEOUT;

        // 设置用户指纹(防止会话劫持)
        $_SESSION['user_fingerprint'] = $this->generateUserFingerprint();
    }

    /**
     * 生成用户指纹(用于检测会话劫持)
     */
    private function generateUserFingerprint() {
        $components = [
            $_SERVER['HTTP_USER_AGENT'] ?? '',
            $_SERVER['REMOTE_ADDR'] ?? '',
            // 可以添加更多组件,但注意隐私问题
];

        return hash('sha256', implode('|', $components));
    }

    /**
     * 验证用户指纹
*/
    public function validateUserFingerprint() {
        if (!isset($_SESSION['user_fingerprint'])) {
            return false;
        }

        $current_fingerprint = $this->generateUserFingerprint();
        return hash_equals($_SESSION['user_fingerprint'], $current_fingerprint);
    }

    /**
     * 检查用户是否已登录
*/
    public function isLoggedIn() {
        if (!isset($_SESSION['user_id'], $_SESSION['expire_time'])) {
            return false;
        }

        // 检查会话是否过期
if (time() > $_SESSION['expire_time']) {
            $this->logout();
            return false;
        }

        // 检查用户指纹(防止会话劫持)
        if (!$this->validateUserFingerprint()) {
            // 记录可疑活动
$this->logSecurityEvent('session_hijack_attempt', [
                'user_id' => $_SESSION['user_id'] ?? 'unknown',
                'ip_address' => $_SERVER['REMOTE_ADDR'] ?? 'unknown'
            ]);

            $this->logout();
            return false;
        }

        // 延长会话过期时间(滑动过期)
        $_SESSION['expire_time'] = time() + SESSION_TIMEOUT;

        return true;
    }

    /**
     * 获取当前用户信息
*/
    public function getCurrentUser() {
        if (!$this->isLoggedIn()) {
            return null;
        }

        $sql = "SELECT id, username, email, full_name, role, status
                FROM users WHERE id = :id";

        $stmt = $this->db->prepare($sql);
        $stmt->execute([':id' => $_SESSION['user_id']]);

        return $stmt->fetch();
    }

    /**
     * 检查用户是否是管理员
*/
    public function isAdmin() {
        if (!$this->isLoggedIn()) {
            return false;
        }

        return $_SESSION['role'] === 'admin';
    }

    /**
     * 用户退出
*/
    public function logout() {
        // 记录安全日志
if (isset($_SESSION['user_id'])) {
            $this->logSecurityEvent('user_logout', [
                'user_id' => $_SESSION['user_id'],
                'ip_address' => $_SERVER['REMOTE_ADDR'] ?? 'unknown'
            ]);
        }

        // 清除所有会话数据
$_SESSION = [];

        // 删除会话cookie
        if (ini_get("session.use_cookies")) {
            $params = session_get_cookie_params();
            setcookie(session_name(), '', time() - 42000,
                $params["path"], $params["domain"],
                $params["secure"], $params["httponly"]
            );
        }

        // 销毁会话
session_destroy();
    }

    /**
     * 验证注册输入
*/
    private function validateRegistrationInput($username, $email, $password) {
        // 用户名验证
if (strlen($username) < 3 || strlen($username) > 50) {
            throw new Exception('用户名长度必须在3-50个字符之间');
        }

        if (!preg_match('/^[a-zA-Z0-9_]+$/', $username)) {
            throw new Exception('用户名只能包含字母、数字和下划线');
        }

        // 邮箱验证
if (!filter_var($email, FILTER_VALIDATE_EMAIL)) {
            throw new Exception('请输入有效的邮箱地址');
        }

        // 密码验证
if (strlen($password) < 8) {
            throw new Exception('密码长度至少8个字符');
        }

        // 密码强度检查
if (!preg_match('/[A-Z]/', $password) ||
            !preg_match('/[a-z]/', $password) ||
            !preg_match('/[0-9]/', $password)) {
            throw new Exception('密码必须包含大小写字母和数字');
        }
    }

    /**
     * 检查用户名是否存在
*/
    private function usernameExists($username) {
        $sql = "SELECT COUNT(*) FROM users WHERE username = :username";
        $stmt = $this->db->prepare($sql);
        $stmt->execute([':username' => $username]);
        return $stmt->fetchColumn() > 0;
    }

    /**
     * 检查邮箱是否存在
*/
    private function emailExists($email) {
        $sql = "SELECT COUNT(*) FROM users WHERE email = :email";
        $stmt = $this->db->prepare($sql);
        $stmt->execute([':email' => $email]);
        return $stmt->fetchColumn() > 0;
    }

    /**
     * 通过用户名获取用户
*/
    private function getUserByUsername($username) {
        $sql = "SELECT * FROM users WHERE username = :username";
        $stmt = $this->db->prepare($sql);
        $stmt->execute([':username' => $username]);
        return $stmt->fetch();
    }

    /**
     * 更新最后登录时间
*/
    private function updateLastLogin($user_id) {
        $sql = "UPDATE users SET last_login = NOW() WHERE id = :id";
        $this->db->prepare($sql)->execute([':id' => $user_id]);
    }

    /**
     * 记录失败登录
*/
    private function recordFailedLogin($username) {
        $ip_address = $_SERVER['REMOTE_ADDR'] ?? 'unknown';
        $key = 'login_failures:' . md5($username . '|' . $ip_address);

        // 使用文件缓存模拟,实际应用中应使用Redis或Memcached
        $cache_file = sys_get_temp_dir() . '/' . $key;

        $failures = 0;
        $first_failure_time = time();

        if (file_exists($cache_file)) {
            $data = json_decode(file_get_contents($cache_file), true);
            if ($data && is_array($data)) {
                $failures = $data['failures'] ?? 0;
                $first_failure_time = $data['first_failure_time'] ?? time();
            }
        }

        $failures++;
        $data = [
            'failures' => $failures,
            'first_failure_time' => $first_failure_time,
            'last_attempt' => time(),
            'username' => $username,
            'ip_address' => $ip_address
        ];

        file_put_contents($cache_file, json_encode($data));

        // 记录安全日志
$this->logSecurityEvent('login_failure', [
            'username' => $username,
            'ip_address' => $ip_address,
            'failure_count' => $failures
        ]);
    }

    /**
     * 清除失败登录记录
*/
    private function clearFailedLogins($username) {
        $ip_address = $_SERVER['REMOTE_ADDR'] ?? 'unknown';
        $key = 'login_failures:' . md5($username . '|' . $ip_address);
        $cache_file = sys_get_temp_dir() . '/' . $key;

        if (file_exists($cache_file)) {
            unlink($cache_file);
        }
    }

    /**
     * 检查登录是否被阻止
*/
    private function isLoginBlocked($username) {
        $ip_address = $_SERVER['REMOTE_ADDR'] ?? 'unknown';
        $key = 'login_failures:' . md5($username . '|' . $ip_address);
        $cache_file = sys_get_temp_dir() . '/' . $key;

        if (!file_exists($cache_file)) {
            return false;
        }

        $data = json_decode(file_get_contents($cache_file), true);
        if (!$data || !is_array($data)) {
            return false;
        }

        $failures = $data['failures'] ?? 0;
        $first_failure_time = $data['first_failure_time'] ?? time();

        // 如果30分钟内失败5次,则锁定15分钟
if ($failures >= 5 && (time() - $first_failure_time) < 1800) {
            // 检查是否已经锁定15分钟
if ((time() - $first_failure_time) < 900) {
                return true;
            }
        }

        return false;
    }

    /**
     * 记录安全事件
*/
    private function logSecurityEvent($event_type, $data) {
        $log_entry = sprintf(
            "[%s] %s: %s\n",
            date('Y-m-d H:i:s'),
            $event_type,
            json_encode($data, JSON_UNESCAPED_UNICODE)
        );

        $log_dir = dirname(__DIR__) . '/logs/';
        if (!is_dir($log_dir)) {
            mkdir($log_dir, 0755, true);
        }

        $log_file = $log_dir . 'security.log';
        file_put_contents($log_file, $log_entry, FILE_APPEND | LOCK_EX);
    }
}
?>
步骤 5:主要页面实现
<?php
// upload.php - 文件上传处理页面
require_once 'includes/config.php';
require_once 'includes/auth.php';
require_once 'includes/uploader.php';

$auth = new Auth();
$uploader = new SecureUploader();

// 检查用户是否登录
if (!$auth->isLoggedIn()) {
    header('Location: login.php?redirect=' . urlencode($_SERVER['REQUEST_URI']));
    exit;
}

$user = $auth->getCurrentUser();
$message = '';
$error = '';

// 处理表单提交
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
    // 验证CSRF Token
    $csrf_token = $_POST[CSRF_TOKEN_NAME] ?? '';
    if (!validate_csrf_token($csrf_token)) {
        $error = '安全验证失败,请重试';
    } else {
        try {
            $title = escape($_POST['title'] ?? '');
            $description = escape($_POST['description'] ?? '');
            $is_public = isset($_POST['is_public']) && $_POST['is_public'] === '1';

            $result = $uploader->upload('image_file', $user['id'], $title, $description, $is_public);

            $message = '文件上传成功!';

            // 如果开启了公开分享,显示分享链接
if ($is_public && $result['share_token']) {
                $share_url = BASE_URL . '/share.php?token=' . $result['share_token'];
                $message .= ' 分享链接: <a href="' . $share_url . '">' . $share_url . '</a>';
            }

        } catch (Exception $e) {
            $error = '上传失败: ' . $e->getMessage();
        }
    }
}

?>
<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>上传图片 - <?php echo APP_NAME; ?></title>
    <link rel="stylesheet" href="assets/css/style.css">
</head>
<body>
    <?php include 'includes/header.php'; ?>

    <div class="container">
        <h1>上传图片</h1>

        <?php if ($message): ?>
            <div class="alert alert-success">
                <?php echo $message; ?>
            </div>
        <?php endif; ?>

        <?php if ($error): ?>
            <div class="alert alert-danger">
                <?php echo $error; ?>
            </div>
        <?php endif; ?>

        <div class="upload-form">
            <form action="upload.php" method="POST" enctype="multipart/form-data" id="uploadForm">
                <?php echo csrf_field(); ?>

                <div class="form-group">
                    <label for="title">图片标题(可选):</label>
                    <input type="text" name="title" id="title" maxlength="200"
                           placeholder="请输入图片标题">
                </div>

                <div class="form-group">
                    <label for="description">描述(可选):</label>
                    <textarea name="description" id="description" rows="3"
                              placeholder="请输入图片描述" maxlength="1000"></textarea>
                </div>

                <div class="form-group">
                    <label for="image_file">选择图片文件:</label>
                    <input type="file" name="image_file" id="image_file"
                           accept=".jpg,.jpeg,.png,.gif,.webp" required>
                    <small class="form-text">
                        仅支持 JPG, PNG, GIF, WebP 格式,最大 5MB
                    </small>
                    <div id="filePreview" class="file-preview"></div>
                </div>

                <div class="form-group">
                    <label class="checkbox-label">
                        <input type="checkbox" name="is_public" value="1" id="is_public">
                        公开分享(生成可分享的链接)
                    </label>
                </div>

                <div class="form-group">
                    <button type="submit" class="btn btn-primary" id="uploadButton">
                        上传图片
</button>
                    <a href="gallery.php" class="btn btn-secondary">返回相册</a>
                </div>
            </form>
        </div>

        <div class="upload-tips">
            <h3>安全提示:</h3>
            <ul>
                <li>系统会对上传的图片进行多重安全校验,包括文件类型、大小、内容等</li>
                <li>上传的图片会被随机重命名,防止文件覆盖攻击</li>
                <li>所有图片文件都存储在安全目录中,无法直接通过URL访问执行</li>
                <li>建议不要上传包含个人隐私信息的图片</li>
            </ul>
        </div>
    </div>

    <?php include 'includes/footer.php'; ?>

    <script src="assets/js/upload.js"></script>
</body>
</html>
步骤 6:相册展示页面
<?php
// gallery.php - 个人相册页面
require_once 'includes/config.php';
require_once 'includes/auth.php';
require_once 'includes/uploader.php';

$auth = new Auth();
$uploader = new SecureUploader();

// 检查用户是否登录
if (!$auth->isLoggedIn()) {
    header('Location: login.php');
    exit;
}

$user = $auth->getCurrentUser();

// 获取用户图片
$page = isset($_GET['page']) ? max(1, intval($_GET['page'])) : 1;
$limit = 12;
$offset = ($page - 1) * $limit;

$images = $uploader->getUserImages($user['id'], $limit, $offset);

// 获取图片总数(用于分页)
$db = Database::getInstance()->getConnection();
$count_sql = "SELECT COUNT(*) FROM images WHERE user_id = :user_id";
$stmt = $db->prepare($count_sql);
$stmt->execute([':user_id' => $user['id']]);
$total_images = $stmt->fetchColumn();
$total_pages = ceil($total_images / $limit);

// 处理删除请求
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['delete_id'])) {
    // 验证CSRF Token
    $csrf_token = $_POST[CSRF_TOKEN_NAME] ?? '';
    if (validate_csrf_token($csrf_token)) {
        try {
            $delete_id = intval($_POST['delete_id']);
            $uploader->deleteImage($delete_id, $user['id']);
            $success_message = '图片删除成功';

            // 刷新页面
header('Location: gallery.php?page=' . $page . '&msg=' . urlencode($success_message));
            exit;
        } catch (Exception $e) {
            $error_message = '删除失败: ' . $e->getMessage();
        }
    } else {
        $error_message = '安全验证失败';
    }
}

// 获取消息参数
$success_message = $_GET['msg'] ?? '';

?>
<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>我的相册 - <?php echo APP_NAME; ?></title>
    <link rel="stylesheet" href="assets/css/style.css">
    <link rel="stylesheet" href="assets/css/gallery.css">
</head>
<body>
    <?php include 'includes/header.php'; ?>

    <div class="container">
        <div class="gallery-header">
            <h1>我的相册</h1>
            <a href="upload.php" class="btn btn-primary">
                <i class="upload-icon"></i> 上传新图片
</a>
        </div>

        <?php if ($success_message): ?>
            <div class="alert alert-success">
                <?php echo htmlspecialchars($success_message); ?>
            </div>
        <?php endif; ?>

        <?php if (isset($error_message)): ?>
            <div class="alert alert-danger">
                <?php echo htmlspecialchars($error_message); ?>
            </div>
        <?php endif; ?>

        <?php if (empty($images)): ?>
            <div class="empty-gallery">
                <div class="empty-icon">🖼️</div>
                <h3>相册空空如也</h3>
                <p>还没有上传任何图片,赶快上传第一张图片吧!</p>
                <a href="upload.php" class="btn btn-primary">上传图片</a>
            </div>
        <?php else: ?>
            <div class="gallery-stats">
                <p><?php echo $total_images; ?> 张图片</p>
            </div>

            <div class="image-grid">
                <?php foreach ($images as $image): ?>
                    <div class="image-card">
                        <div class="image-preview">
                            <?php
                            $thumbnail_path = str_replace(UPLOAD_BASE_DIR, 'private_uploads/',
                                        UPLOAD_BASE_DIR . THUMBNAIL_DIR . $image['stored_name']);
                            ?>
                            <img src="<?php echo $thumbnail_path; ?>"
                                 alt="<?php echo escape($image['title'] ?: $image['original_name']); ?>"
                                 loading="lazy">
                        </div>

                        <div class="image-info">
                            <h4><?php echo escape($image['title'] ?: '未命名图片'); ?></h4>
                            <p class="image-meta">
                                <?php echo date('Y-m-d H:i', strtotime($image['uploaded_at'])); ?> |
                                <?php echo round($image['file_size'] / 1024); ?> KB
                            </p>

                            <?php if ($image['is_public'] && $image['share_token']): ?>
                                <p class="share-info">
                                    <span class="share-badge">公开</span>
                                    <a href="share.php?token=<?php echo $image['share_token']; ?>"
                                       target="_blank" class="share-link">
                                        分享链接
</a>
                                </p>
                            <?php endif; ?>

                            <div class="image-actions">
                                <button type="button" class="btn btn-sm btn-info view-btn"
                                        data-image-id="<?php echo $image['id']; ?>">
                                    查看
</button>

                                <form method="POST" class="delete-form"
                                      onsubmit="return confirm('确定要删除这张图片吗?');">
                                    <?php echo csrf_field(); ?>
                                    <input type="hidden" name="delete_id" value="<?php echo $image['id']; ?>">
                                    <button type="submit" class="btn btn-sm btn-danger">
                                        删除
</button>
                                </form>
                            </div>
                        </div>
                    </div>
                <?php endforeach; ?>
            </div>

            <!-- 分页 -->
            <?php if ($total_pages > 1): ?>
                <nav class="pagination">
                    <ul>
                        <?php if ($page > 1): ?>
                            <li><a href="?page=<?php echo $page - 1; ?>">上一页</a></li>
                        <?php endif; ?>

                        <?php for ($i = 1; $i <= $total_pages; $i++): ?>
                            <?php if ($i == $page): ?>
                                <li class="active"><span><?php echo $i; ?></span></li>
                            <?php else: ?>
                                <li><a href="?page=<?php echo $i; ?>"><?php echo $i; ?></a></li>
                            <?php endif; ?>
                        <?php endfor; ?>

                        <?php if ($page < $total_pages): ?>
                            <li><a href="?page=<?php echo $page + 1; ?>">下一页</a></li>
                        <?php endif; ?>
                    </ul>
                </nav>
            <?php endif; ?>

        <?php endif; ?>
    </div>

    <!-- 图片查看模态框 -->
    <div id="imageModal" class="modal">
        <div class="modal-content">
            <span class="close-modal">&times;</span>
            <div id="modalImageContainer"></div>
            <div id="modalImageInfo"></div>
        </div>
    </div>

    <?php include 'includes/footer.php'; ?>

    <script src="assets/js/gallery.js"></script>
</body>
</html>

项目测试与部署指南

1. 环境准备
# 安装必要的PHP扩展
sudo apt-get install php php-mysql php-gd php-mbstring php-xml

# 检查扩展是否启用
php -m | grep -E "mysql|gd|mbstring"

# 创建项目目录结构
mkdir -p secure_gallery/{includes,uploads,assets/{css,js,images},logs}
mkdir -p private_uploads/{images,thumbs}

# 设置目录权限
chmod 755 secure_gallery
chmod 755 private_uploads
chmod 755 private_uploads/images
chmod 755 private_uploads/thumbs
chmod 644 private_uploads/.htaccess
chmod 644 private_uploads/images/.htaccess
chmod 644 private_uploads/thumbs/.htaccess

# 创建日志目录并设置权限
mkdir -p logs
chmod 755 logs
chmod 644 logs/security.log
2. 数据库配置
-- 执行数据库初始化脚本(见步骤1)
-- 修改includes/config.php中的数据库连接配置
3. 安全配置检查清单

创建安全检查脚本 check_security.php:

<?php
// 安全检查脚本
echo "=== PHP安全配置检查 ===\n\n";

// 1. 检查PHP版本
echo "1. PHP版本: " . PHP_VERSION . "\n";
if (version_compare(PHP_VERSION, '7.4.0') < 0) {
    echo "     建议升级到PHP 7.4或更高版本\n";
}

// 2. 检查必要的扩展
$required_extensions = ['pdo_mysql', 'gd', 'mbstring', 'openssl'];
foreach ($required_extensions as $ext) {
    echo "2. 扩展 {$ext}: " . (extension_loaded($ext) ? "✓ 已启用" : "✗ 未启用") . "\n";
}

// 3. 检查重要的PHP配置
$important_settings = [
    'allow_url_fopen' => 'Off',
    'allow_url_include' => 'Off',
    'display_errors' => 'Off',
    'log_errors' => 'On',
    'expose_php' => 'Off',
    'session.cookie_httponly' => '1',
    'session.cookie_secure' => '1',
    'session.use_only_cookies' => '1'
];

foreach ($important_settings as $setting => $recommended) {
    $current = ini_get($setting);
    echo "3. {$setting}: {$current}";
    if ($current == $recommended) {
        echo " ✓ 安全\n";
    } else {
        echo "   建议设置为: {$recommended}\n";
    }
}

// 4. 检查文件权限
$directories_to_check = [
    'private_uploads' => 0755,
    'private_uploads/images' => 0755,
    'private_uploads/thumbs' => 0755,
    'logs' => 0755
];

foreach ($directories_to_check as $dir => $recommended_perm) {
    if (is_dir($dir)) {
        $perms = fileperms($dir) & 0777;
        echo "4. 目录权限 {$dir}: " . decoct($perms);
        if ($perms == $recommended_perm) {
            echo " ✓ 安全\n";
        } else {
            echo "   建议设置为: " . decoct($recommended_perm) . "\n";
        }
    } else {
        echo "4. 目录 {$dir}: ✗ 不存在\n";
    }
}

// 5. 检查.htaccess文件
$htaccess_files = [
    'private_uploads/.htaccess',
    'private_uploads/images/.htaccess',
    'private_uploads/thumbs/.htaccess'
];

foreach ($htaccess_files as $file) {
    if (file_exists($file)) {
        echo "5. 安全文件 {$file}: ✓ 存在\n";

        // 检查内容
$content = file_get_contents($file);
        if (strpos($content, 'Deny from all') !== false ||
            strpos($content, 'Order Deny,Allow') !== false) {
            echo "   ✓ 包含基本安全规则\n";
        }
    } else {
        echo "5. 安全文件 {$file}: ✗ 不存在\n";
    }
}

echo "\n=== 检查完成 ===\n";
echo "✓ 表示安全配置正确\n";
echo "  表示需要关注或优化\n";
echo "✗ 表示存在安全问题\n";
?>
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

霸王大陆

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值