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

PHP 多檔案上傳類別完整實作與安全強化指南

     編輯:PHP 綜合知識

前言

檔案上傳是 Web 開發高風險功能,需嚴格驗證檔案類型、大小、內容、儲存路徑。本文提供一個結構化的多檔案上傳類別,並詳細說明安全防護要點。

上傳類別核心實作

<?php
/**
 * 多檔案上傳處理類
 * 功能:類型驗證、真實格式檢測、隨機重命名、大小限制、錯誤處理
 */
class MultiFileUpload {
    // ========== 配置常數 ==========
    const UPLOAD_DIR = __DIR__ . '/../uploads/';      // 儲存目錄(需可寫入)
    const MAX_FILE_SIZE = 5 * 1024 * 1024;            // 5MB 單檔上限
    const MAX_TOTAL_SIZE = 20 * 1024 * 1024;          // 20MB 總上限
    const ALLOWED_MIME = [                            // 允許的 MIME 類型
        'image/jpeg', 'image/png', 'image/gif', 'image/webp',
        'application/pdf', 'text/plain',
    ];
    const ALLOWED_EXT = ['jpg', 'jpeg', 'png', 'gif', 'webp', 'pdf', 'txt']; // 允許副檔名
    const MAGIC_BYTES = [                             // 檔案簽名(Magic Bytes)驗證
        'jpg' => [0xFF, 0xD8, 0xFF],
        'png' => [0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A],
        'gif' => [0x47, 0x49, 0x46, 0x38],
        'pdf' => [0x25, 0x50, 0x44, 0x46], // %PDF
    ];

    // ========== 狀態屬性 ==========
    private $files = [];           // $_FILES 結構化資料
    private $results = [];         // 處理結果
    private $totalSize = 0;        // 總大小統計
    private $errors = [];          // 錯誤收集

    // ========== 公開介面 ==========
    public function __construct() {
        // 確保上傳目錄存在
        if (!is_dir(self::UPLOAD_DIR)) {
            mkdir(self::UPLOAD_DIR, 0755, true);
        }
    }

    /**
     * 處理上傳
     * @param string $fieldName 表單檔案欄位名(預設 'files')
     * @return array 處理結果陣列
     */
    public function handleUpload($fieldName = 'files') {
        $this->normalizeFiles($fieldName);
        $this->validateGlobalConstraints();
        
        foreach ($this->files as $file) {
            $this->processSingleFile($file);
        }
        
        return $this->results;
    }

    // ========== 內部處理邏輯 ==========
    private function normalizeFiles($fieldName) {
        $rawFiles = $_FILES[$fieldName] ?? [];
        if (empty($rawFiles['name'])) return;

        // 處理多檔案陣列結構(name、type、tmp_name、error、size 為平行陣列)
        $count = count($rawFiles['name']);
        for ($i = 0; $i < $count; $i++) {
            if ($rawFiles['error'][$i] === UPLOAD_ERR_NO_FILE) continue;
            
            $this->files[] = [
                'name' => $rawFiles['name'][$i],
                'type' => $rawFiles['type'][$i],
                'tmp_name' => $rawFiles['tmp_name'][$i],
                'error' => $rawFiles['error'][$i],
                'size' => $rawFiles['size'][$i],
                'ext' => strtolower(pathinfo($rawFiles['name'][$i], PATHINFO_EXTENSION)),
            ];
        }
    }

    private function validateGlobalConstraints() {
        $this->totalSize = array_sum(array_column($this->files, 'size'));
        if ($this->totalSize > self::MAX_TOTAL_SIZE) {
            throw new Exception("總上傳大小超過限制 (" . $this->formatBytes(self::MAX_TOTAL_SIZE) . ")");
        }
    }

    private function processSingleFile($file) {
        $result = [
            'original_name' => $file['name'],
            'size' => $file['size'],
            'size_formatted' => $this->formatBytes($file['size']),
            'success' => false,
            'message' => '',
            'saved_path' => '',
            'saved_name' => '',
        ];

        // 1. PHP 上傳錯誤碼檢查
        if ($file['error'] !== UPLOAD_ERR_OK) {
            $result['message'] = $this->getUploadErrorMessage($file['error']);
            $this->results[] = $result;
            return;
        }

        // 2. 檔案大小檢查
        if ($file['size'] > self::MAX_FILE_SIZE) {
            $result['message'] = "檔案過大,上限 " . $this->formatBytes(self::MAX_FILE_SIZE);
            $this->results[] = $result;
            return;
        }

        // 3. 副檔名檢查
        if (!in_array($file['ext'], self::ALLOWED_EXT)) {
            $result['message'] = "不允許的副檔名: {$file['ext']}";
            $this->results[] = $result;
            return;
        }

        // 4. MIME 類型檢查(瀏覽器提供,不可全信,但作為第一道防線)
        if (!in_array($file['type'], self::ALLOWED_MIME)) {
            $result['message'] = "不允許的 MIME 類型: {$file['type']}";
            $this->results[] = $result;
            return;
        }

        // 5. 真實檔案格式驗證(Magic Bytes / 檔案簽名)
        if (!$this->verifyFileSignature($file['tmp_name'], $file['ext'])) {
            $result['message'] = "檔案內容與副檔名不符,可能偽裝檔案";
            $this->results[] = $result;
            return;
        }

        // 6. 圖片額外驗證(使用 getimagesize 確認真實圖片)
        if (in_array($file['ext'], ['jpg','jpeg','png','gif','webp'])) {
            $imgInfo = @getimagesize($file['tmp_name']);
            if (!$imgInfo) {
                $result['message'] = "無效的圖片檔案";
                $this->results[] = $result;
                return;
            }
            // 可選:限制圖片尺寸
            // if ($imgInfo[0] > 5000 || $imgInfo[1] > 5000) { ... }
        }

        // 7. 生成安全檔名(隨機 + 保留副檔名)
        $newName = bin2hex(random_bytes(16)) . '.' . $file['ext'];
        $destPath = self::UPLOAD_DIR . $newName;

        // 8. 移動檔案
        if (!move_uploaded_file($file['tmp_name'], $destPath)) {
            $result['message'] = "檔案儲存失敗(權限或磁碟空間問題)";
            $this->results[] = $result;
            return;
        }

        // 9. 設定權限(僅擁有者可讀寫)
        @chmod($destPath, 0644);

        // 成功
        $result['success'] = true;
        $result['message'] = '上傳成功';
        $result['saved_path'] = $destPath;
        $result['saved_name'] = $newName;
        $this->results[] = $result;
    }

    // ========== 工具方法 ==========
    private function verifyFileSignature($tmpPath, $ext) {
        $signatures = self::MAGIC_BYTES[$ext] ?? null;
        if (!$signatures) return true; // 無簽名定義則跳過

        $handle = fopen($tmpPath, 'rb');
        $header = fread($handle, max(8, count($signatures)));
        fclose($handle);

        foreach ($signatures as $offset => $byte) {
            if (ord($header[$offset]) !== $byte) {
                return false;
            }
        }
        return true;
    }

    private function getUploadErrorMessage($code) {
        $messages = [
            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 擴充功能阻止',
        ];
        return $messages[$code] ?? "未知上傳錯誤代碼: {$code}";
    }

    private function formatBytes($bytes, $precision = 2) {
        $units = ['B', 'KB', 'MB', 'GB', 'TB'];
        $bytes = max($bytes, 0);
        $pow = floor(($bytes ? log($bytes) : 0) / log(1024));
        $pow = min($pow, count($units) - 1);
        $value = $bytes / (1024 ** $pow);
        return round($value, $precision) . ' ' . $units[$pow];
    }
}
?>

HTML 表單與控制器整合

<!-- HTML 表單(注意 enctype)-->
<form action="upload.php" method="POST" enctype="multipart/form-data">
    <input type="hidden" name="MAX_FILE_SIZE" value="5242880"> <!-- 5MB,瀏覽器端提示用 -->
    <input type="file" name="files[]" multiple accept="image/*,.pdf,.txt">
    <button type="submit">上傳</button>
</form>

<?php
// upload.php
require 'MultiFileUpload.php';

$uploader = new MultiFileUpload();
try {
    $results = $uploader->handleUpload('files');
    
    // 輸出 JSON 供前端處理
    header('Content-Type: application/json');
    echo json_encode([
        'success' => true,
        'data' => $results,
    ], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
} catch (Exception $e) {
    http_response_code(400);
    echo json_encode([
        'success' => false,
        'message' => $e->getMessage(),
    ]);
}
?>

安全防護核心清單

防護層級措施說明
1. 客戶端accept、MAX_FILE_SIZE僅用戶體驗提示,不可信任
2. 伺服器端 - 基礎$_FILES['error']、size、副檔名、MIME基本過濾,易繞過
3. 伺服器端 - 核心Magic Bytes 檔案簽名驗證確認檔案真實格式,防偽裝
4. 伺服器端 - 進階getimagesize()、解析器驗證(PDF 解析等)深度內容驗證
5. 儲存安全隨機檔名、存放 Web 根目錄外、權限 0644、禁止執行防止直接存取、腳本執行
6. 傳輸安全HTTPS、CSRF Token、驗證登入狀態防竄改、防偽造請求

關鍵配置建議(php.ini / .htaccess / Nginx)

; php.ini
file_uploads = On
upload_max_filesize = 10M
post_max_size = 20M
max_file_uploads = 20
max_execution_time = 60
memory_limit = 128M
; upload_tmp_dir = /tmp/secure_uploads  ; 建議指定專用暫存目錄
# Nginx 限制上傳大小
client_max_body_size 20M;

# 上傳目錄禁止腳本執行
location /uploads/ {
    location ~ \.(php|php5|phtml|phar)$ {
        deny all;
        return 404;
    }
}

進階擴展方向

  1. 分片上傳/斷點續傳: 前端切片,後端合併(大檔案必備)
  2. 雲端儲存整合: 直接上傳至 S3/OSS/COS(預簽名 URL),減輕伺服器壓力
  3. 病毒掃描: 整合 ClamAV 掃描上傳檔案
  4. 圖片處理管線: 上傳後自動生成縮圖、WebP 轉換、水印添加
  5. 異步處理: 寫入佇列,Worker 處理耗時操作(轉碼、OCR、索引)

檔案上傳看似簡單,實則是安全攻防前線。建立分層驗證、最小權限儲存、完整審計日誌的上傳體系,是每個後端工程師的必修課。

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