首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
您现在的位置: 程式碼之家 >> 編程語言 >  >> 網頁程式設計 >> PHP 程式設計 >> PHP 相關知識

PHP 文件上傳完整實戰:從表單構建到安全存儲與列表展示

     編輯:PHP 相關知識

前言

文件上傳是 Web 開發的基礎功能,但也是安全漏洞高發區。本文提供一個完整、可直接運行的單文件上傳示例,涵蓋表單構建、多重安全驗證、文件存儲、已上傳文件列表展示與下載鏈接生成,並詳細講解每個安全控制點。

完整代碼結構(單文件 upload.php)

<!DOCTYPE html>
<html>
<head>
    <meta charset="UTF-8">
    <title>PHP 文件上傳示例</title>
    <style>
        body { font-family: sans-serif; max-width: 800px; margin: 2rem auto; }
        .file-list a { display: block; margin: 0.5rem 0; }
        .msg { padding: 0.5rem; margin: 1rem 0; border-radius: 4px; }
        .success { background: #d4edda; color: #155724; border: 1px solid #c3e6cb; }
        .error { background: #f8d7da; color: #721c24; border: 1px solid #f5c6cb; }
    </style>
</head>
<body>
    <h1>文件上傳</h1>
    
    <!-- 上傳表單:enctype 必須為 multipart/form-data -->
    <form method="post" enctype="multipart/form-data">
        <label>選擇文件: <input type="file" name="upload_file" required></label>
        <button type="submit">上傳</button>
    </form>

    <hr>
    <h2>已上傳文件</h2>
    <div class="file-list">
        <?php
        // ========== 配置區 ==========
        $storeDir = $_SERVER['DOCUMENT_ROOT'] . '/uploads/'; // 存儲目錄
        $maxSize = 200 * 1024 * 1024; // 200MB
        $allowOverwrite = false;      // 是否允許覆蓋同名文件
        // ============================

        // 確保目錄存在
        if (!is_dir($storeDir)) {
            mkdir($storeDir, 0755, true);
        }

        // 遞歸獲取文件列表(含子目錄)
        function getFileList(string $dir): array {
            $files = [];
            $iterator = new RecursiveIteratorIterator(
                new RecursiveDirectoryIterator($dir, FilesystemIterator::SKIP_DOTS)
            );
            foreach ($iterator as $file) {
                if ($file->isFile()) {
                    $files[] = $file->getRealPath();
                }
            }
            return $files;
        }

        // 顯示文件列表
        $fileList = getFileList($storeDir);
        foreach ($fileList as $index => $fullPath) {
            // 生成相對 Web 根目錄的 URL 路徑
            $relPath = '/' . ltrim(str_replace($_SERVER['DOCUMENT_ROOT'], '', $fullPath), '/');
            echo '<a href="' . htmlspecialchars($relPath) . '" target="_blank">';
            echo htmlspecialchars(basename($fullPath)) . ' (' . formatBytes(filesize($fullPath)) . ')</a>';
        }
        
        function formatBytes(int $bytes): string {
            $units = ['B', 'KB', 'MB', 'GB'];
            $i = 0;
            while ($bytes >= 1024 && $i < count($units)-1) {
                $bytes /= 1024;
                $i++;
            }
            return round($bytes, 2) . ' ' . $units[$i];
        }
        ?>
    </div>

    <?php
    // ========== 上傳處理邏輯(POST 請求時執行) ==========
    if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_FILES['upload_file'])) {
        $file = $_FILES['upload_file'];
        
        // 1. 檢查上傳錯誤碼
        if ($file['error'] !== UPLOAD_ERR_OK) {
            $errors = [
                UPLOAD_ERR_INI_SIZE   => '超過 php.ini upload_max_filesize 限制',
                UPLOAD_ERR_FORM_SIZE  => '超過表單 MAX_FILE_SIZE 限制',
                UPLOAD_ERR_PARTIAL    => '文件只有部分被上傳',
                UPLOAD_ERR_NO_FILE    => '沒有文件被上傳',
                UPLOAD_ERR_NO_TMP_DIR => '缺少臨時目錄',
                UPLOAD_ERR_CANT_WRITE => '寫入臨時文件失敗',
                UPLOAD_ERR_EXTENSION  => '被 PHP 擴展阻止',
            ];
            $msg = $errors[$file['error']] ?? '未知上傳錯誤';
            echo '<div class="msg error">' . htmlspecialchars($msg) . '</div>';
        } else {
            // 2. 文件大小二次驗證(防範偽造請求)
            if ($file['size'] > $maxSize) {
                echo '<div class="msg error">文件大小超過限制 (' . formatBytes($maxSize) . ')</div>';
            }
            // 3. 文件名安全處理
            $originalName = basename($file['name']);
            $safeName = preg_replace('/[^a-zA-Z0-9._-]/', '_', $originalName); // 過濾危險字符
            $targetPath = $storeDir . $safeName;
            
            // 4. 重名檢查
            if (file_exists($targetPath) && !$allowOverwrite) {
                echo '<div class="msg error">文件已存在,不允許覆蓋</div>';
            } else {
                // 5. 移動文件(核心步驟:從臨時目錄移至目標目錄)
                if (move_uploaded_file($file['tmp_name'], $targetPath)) {
                    echo '<div class="msg success">上傳成功:' . htmlspecialchars($safeName) . '</div>';
                    // 可選:刷新頁面顯示新列表
                    echo '<script>setTimeout(() => location.reload(), 1000);</script>';
                } else {
                    echo '<div class="msg error">文件移動失敗,請檢查目錄權限</div>';
                }
            }
        }
        
        // 顯示文件元信息(調試用)
        echo '<h3>文件信息</h3><pre>';
        print_r([
            'name' => $file['name'],
            'type' => $file['type'],        // MIME 類型(瀏覽器提供,不可信)
            'size' => $file['size'],
            'tmp_name' => $file['tmp_name'],
            'error' => $file['error'],
        ]);
        echo '</pre>';
    }
    ?>
</body>
</html>

核心安全控制點深度解析

1. 表單 enctype="multipart/form-data"

缺少此屬性將導致 $_FILES 為空,文件無法上傳。

2. 上傳錯誤碼完整處理

$_FILES['file']['error'] 是判斷上傳是否成功的**唯一可靠標準**,不可僅靠 file_exists() 或 filesize() 判斷。

3. 文件大小雙重限制

  • 服務端配置:php.ini 中 upload_max_filesize、post_max_size(需 > upload_max_filesize)。
  • 代碼邏輯:業務層再次校驗 $file['size'],防範繞過客戶端限制的惡意請求。

4. 文件名安全化

// 移除路徑遍歷風險、特殊字符
$safeName = preg_replace('/[^a-zA-Z0-9._-]/', '_', basename($file['name']));
// 更安全:生成隨機文件名 + 保留副檔名
$ext = pathinfo($file['name'], PATHINFO_EXTENSION);
$safeName = uniqid('upload_') . ($ext ? '.' . $ext : '');

原則:絕不信任原始文件名,存儲時使用隨機名,原始名存入數據庫供下載時顯示。

5. MIME 類型不可信

$file['type'] 由瀏覽器根據副檔名猜測,極易偽造。真實類型檢測需用:

// 方法一:finfo(推薦,需 fileinfo 擴展)
$finfo = new finfo(FILEINFO_MIME_TYPE);
$realMime = $finfo->file($file['tmp_name']);
$allowed = ['image/jpeg', 'image/png', 'application/pdf'];
if (!in_array($realMime, $allowed)) die('文件類型不允許');

// 方法二:getimagesize()(僅圖片)
$info = getimagesize($file['tmp_name']);
if ($info === false) die('非合法圖片文件');

6. 存儲目錄權限與隔離

  • 目錄權限 0755(擁有者可寫,組/其他可讀執行),避免 0777。
  • 上傳目錄**禁用腳本執行權限**(Nginx/Apache 配置 location /uploads { php_flag engine off; })。
  • 最佳實踐:將上傳文件存放在 **Web 根目錄外**,通過 PHP 讀取輸出(readfile())提供下載,徹底杜絕腳本執行風險。

7. 防止大文件阻塞/DoS

  • 設置合理的 max_execution_time、memory_limit。
  • 大文件分片上傳(前端切片,後端合併),避免單次請求超時。
  • 限制單用戶並發上傳數、總存儲配額。

進階擴展方向

  1. 分片上傳/斷點續傳:前端使用 Plupload/Uppy/WebUploader,後端合併臨時分片。
  2. 圖片處理:上傳後自動縮略圖、水印、格式轉換(Intervention Image / GD / Imagick)。
  3. 雲存儲對接:直接上傳至 OSS/S3/COS,服務端只簽名授權,減輕帶寬壓力。
  4. 病毒掃描:集成 ClamAV 掃描上傳文件。
  5. 元數據管理:文件信息(原名、類型、大小、上傳者、業務關聯 ID)入庫,構建文件管理系統。

總結

文件上傳看似簡單,實則涉及協議解析、文件系統權限、MIME 偽造防範、存儲架構設計等多層知識。掌握 $_FILES 結構、move_uploaded_file() 原子操作、finfo 真實類型檢測、隨機文件名生成、存儲目錄隔離這五大核心技術點,即可構建安全可靠的上傳功能。實際項目中,建議封裝為可復用的 Uploader 服務類,統一錯誤處理、日志記錄、事件分發(如上傳成功觸發圖片處理隊列),提升可維護性與擴展性。

  1. 上一篇文章:
  2. 下一篇文章:
Copyright © 程式碼之家 All Rights Reserved