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

PHP 多文件上傳類完整實現:支持圖片驗證、類型檢測、大小限制與擴展架構

     編輯:PHP 相關知識

文件上傳是 Web 開發的基礎功能,但安全性、驗證邏輯、擴展性往往被忽視。本文提供一個生產級的 PHP 多文件上傳類,包含 MIME 類型驗證、文件頭魔數檢測、大小限制、隨機命名、單位轉換等完整功能,並採用面向對象設計,易於擴展支持任意文件類型。

核心功能特性

  • 支持多文件並發上傳($_FILES 數組遍歷)
  • 雙重驗證:MIME 類型 + 文件頭魔數(防偽造上傳)
  • 可配置允許類型、最大尺寸、上傳目錄
  • 自動生成唯一文件名(MD5 + uniqid + 微秒)防衝突
  • 文件大小人性化顯示(B/KB/MB 自動轉換)
  • 完整錯誤碼處理與友好提示
  • 靜態屬性統計總上傳大小

完整類代碼

<?php
/**
 * 多文件上傳處理類
 * 支持圖片/文檔/壓縮包等任意類型,核心驗證邏輯可擴展
 */
class MultiFileUpload
{
    // ===== 可配置屬性 =====
    public string $uploadDir = '/uploadfile/';          // 上傳根目錄(需可寫權限)
    public array $allowedMimeTypes = [                  // 允許的 MIME 類型
        'image/gif', 'image/jpeg', 'image/png', 'image/webp',
        'application/pdf', 'text/plain',
        'application/zip', 'application/x-rar-compressed',
    ];
    public array $allowedExtensions = [                 // 允許的副檔名(二次驗證)
        'gif', 'jpg', 'jpeg', 'png', 'webp',
        'pdf', 'txt', 'zip', 'rar',
    ];
    public int $maxFileSize = 10 * 1024 * 1024;         // 10MB 單文件限制
    public int $maxTotalSize = 50 * 1024 * 1024;        // 50MB 總上傳限制

    // ===== 內部狀態 =====
    private array $files = [];                          // $_FILES 規範化數據
    private array $results = [];                        // 處理結果
    private int $totalSize = 0;                         // 本次上傳總大小

    // ===== 文件頭魔數特徵庫(十六進制簽名)=====
    private array $magicSignatures = [
        'jpeg' => ['ffd8ff'],           // JPEG: FF D8 FF
        'png'  => ['89504e47'],         // PNG: 89 50 4E 47
        'gif'  => ['47494638'],         // GIF: 47 49 46 38 (GIF8)
        'pdf'  => ['25504446'],         // PDF: 25 50 44 46 (%PDF)
        'zip'  => ['504b0304', '504b0506', '504b0708'], // ZIP 多種簽名
        'rar'  => ['52617221'],         // RAR: 52 61 72 21 (Rar!)
    ];

    /**
     * 構造函數:接收 $_FILES 並規範化
     */
    public function __construct(array $files = [])
    {
        $this->files = $this->normalizeFiles($files ?: $_FILES);
    }

    /**
     * 執行上傳處理
     * @return array ['success' => [...], 'failed' => [...], 'total_size' => int]
     */
    public function upload(): array
    {
        $this->results = ['success' => [], 'failed' => []];
        $this->totalSize = 0;

        foreach ($this->files as $index => $file) {
            $result = $this->processSingleFile($file, $index);
            if ($result['success']) {
                $this->results['success'][] = $result;
                $this->totalSize += $file['size'];
            } else {
                $this->results['failed'][] = $result;
            }
        }

        $this->results['total_size'] = $this->totalSize;
        return $this->results;
    }

    /**
     * 規範化 $_FILES 結構,支持單文件、多文件、多字段
     */
    private function normalizeFiles(array $rawFiles): array
    {
        $normalized = [];
        foreach ($rawFiles as $fieldName => $fileData) {
            if (isset($fileData['name']) && is_array($fileData['name'])) {
                // 多文件字段:name="files[]"
                $count = count($fileData['name']);
                for ($i = 0; $i < $count; $i++) {
                    $normalized[] = [
                        'field' => $fieldName,
                        'name' => $fileData['name'][$i],
                        'type' => $fileData['type'][$i],
                        'tmp_name' => $fileData['tmp_name'][$i],
                        'error' => $fileData['error'][$i],
                        'size' => $fileData['size'][$i],
                    ];
                }
            } else {
                // 單文件字段
                $normalized[] = [
                    'field' => $fieldName,
                    'name' => $fileData['name'],
                    'type' => $fileData['type'],
                    'tmp_name' => $fileData['tmp_name'],
                    'error' => $fileData['error'],
                    'size' => $fileData['size'],
                ];
            }
        }
        return $normalized;
    }

    /**
     * 處理單個文件完整流程
     */
    private function processSingleFile(array $file, int $index): array
    {
        $baseResult = [
            'index' => $index,
            'field' => $file['field'],
            'original_name' => $file['name'],
            'size' => $file['size'],
            'size_human' => $this->formatSize($file['size']),
            'mime' => $file['type'],
        ];

        // 1. 上傳錯誤碼檢查
        if ($file['error'] !== UPLOAD_ERR_OK) {
            return array_merge($baseResult, [
                'success' => false,
                'error_code' => $file['error'],
                'message' => $this->getUploadErrorMessage($file['error']),
            ]);
        }

        // 2. 文件大小檢查
        if ($file['size'] > $this->maxFileSize) {
            return array_merge($baseResult, [
                'success' => false,
                'message' => "文件大小超過限制 ({$this->formatSize($this->maxFileSize)})",
            ]);
        }

        // 3. 總大小檢查
        if (($this->totalSize + $file['size']) > $this->maxTotalSize) {
            return array_merge($baseResult, [
                'success' => false,
                'message' => "總上傳大小超過限制 ({$this->formatSize($this->maxTotalSize)})",
            ]);
        }

        // 4. MIME 類型檢查
        if (!in_array($file['type'], $this->allowedMimeTypes, true)) {
            return array_merge($baseResult, [
                'success' => false,
                'message' => "不支持的文件類型: {$file['type']}",
            ]);
        }

        // 5. 副檔名檢查
        $ext = strtolower(pathinfo($file['name'], PATHINFO_EXTENSION));
        if (!in_array($ext, $this->allowedExtensions, true)) {
            return array_merge($baseResult, [
                'success' => false,
                'message' => "不支持的文件副檔名: .{$ext}",
            ]);
        }

        // 6. 文件頭魔數驗證(核心安全防禦)
        if (!$this->verifyMagicNumber($file['tmp_name'], $ext)) {
            return array_merge($baseResult, [
                'success' => false,
                'message' => "文件內容與聲明類型不符,疑似偽造上傳",
            ]);
        }

        // 7. 生成唯一文件名並移動
        $newName = $this->generateUniqueName($ext);
        $destPath = rtrim($this->uploadDir, '/') . '/' . $newName;

        // 確保目錄存在
        $dir = dirname($destPath);
        if (!is_dir($dir) && !mkdir($dir, 0755, true)) {
            return array_merge($baseResult, [
                'success' => false,
                'message' => "無法創建上傳目錄: {$dir}",
            ]);
        }

        if (!move_uploaded_file($file['tmp_name'], $destPath)) {
            return array_merge($baseResult, [
                'success' => false,
                'message' => "文件移動失敗,請檢查目錄權限",
            ]);
        }

        // 8. 成功返回
        return array_merge($baseResult, [
            'success' => true,
            'saved_name' => $newName,
            'saved_path' => $destPath,
            'url' => $this->getPublicUrl($newName), // 需根據實際部署實現
            'message' => '上傳成功',
        ]);
    }

    /**
     * 文件頭魔數驗證:讀取前 8-12 字節匹配特徵碼
     */
    private function verifyMagicNumber(string $tmpPath, string $ext): bool
    {
        if (!isset($this->magicSignatures[$ext])) {
            return true; // 未定義特徵碼的類型跳過驗證(或可選擇拒絕)
        }

        $handle = fopen($tmpPath, 'rb');
        if (!$handle) return false;

        $header = fread($handle, 12); // 讀取足夠字節覆蓋所有特徵碼
        fclose($handle);

        if ($header === false || strlen($header) < 4) return false;

        $hexHeader = bin2hex($header);
        foreach ($this->magicSignatures[$ext] as $signature) {
            if (str_starts_with($hexHeader, $signature)) {
                return true;
            }
        }
        return false;
    }

    /**
     * 生成唯一文件名:md5(uniqid + microtime) + 副檔名
     */
    private function generateUniqueName(string $ext): string
    {
        return md5(uniqid((string)microtime(true), true)) . '.' . $ext;
    }

    /**
     * 文件大小人性化格式化
     */
    private function formatSize(int $bytes): string
    {
        if ($bytes < 1024) return "{$bytes} B";
        if ($bytes < 1024 * 1024) return round($bytes / 1024, 1) . " KB";
        if ($bytes < 1024 * 1024 * 1024) return round($bytes / (1024 * 1024), 1) . " MB";
        return round($bytes / (1024 * 1024 * 1024), 1) . " GB";
    }

    /**
     * 上傳錯誤碼轉中文提示
     */
    private function getUploadErrorMessage(int $code): string
    {
        return match ($code) {
            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 擴展阻止',
            default => "未知上傳錯誤 (代碼: {$code})",
        };
    }

    /**
     * 獲取公開訪問 URL(需根據實際部署覆寫)
     */
    protected function getPublicUrl(string $filename): string
    {
        // 示例:return "https://cdn.example.com/uploads/{$filename}";
        return "/uploads/{$filename}";
    }
}

使用示例

HTML 表單

<form action="upload.php" method="post" enctype="multipart/form-data">
    <input type="file" name="images[]" multiple accept="image/*,.pdf,.zip">
    <input type="file" name="documents[]" multiple accept=".pdf,.doc,.txt">
    <button type="submit">上傳</button>
</form>

PHP 處理邏輯

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

$uploader = new MultiFileUpload();

// 可選:自定義配置
$uploader->uploadDir = __DIR__ . '/storage/uploads/';
$uploader->maxFileSize = 5 * 1024 * 1024; // 5MB
$uploader->allowedMimeTypes = ['image/jpeg', 'image/png', 'image/webp', 'application/pdf'];
$uploader->allowedExtensions = ['jpg', 'jpeg', 'png', 'webp', 'pdf'];

$results = $uploader->upload();

// 輸出結果
header('Content-Type: application/json');
echo json_encode([
    'success_count' => count($results['success']),
    'failed_count' => count($results['failed']),
    'total_size' => $results['total_size'],
    'total_size_human' => (new MultiFileUpload())->formatSize($results['total_size']),
    'details' => $results,
], JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT);

擴展指南

1. 添加新文件類型支持

// 繼承並擴展魔數特徵庫
class ExtendedUpload extends MultiFileUpload
{
    protected array $magicSignatures = [
        // 父類簽名...
        'docx' => ['504b0304'],     // Office Open XML (實際是 ZIP 結構)
        'xlsx' => ['504b0304'],
        'pptx' => ['504b0304'],
        'mp4'  => ['6674797069736f6d'], // ftypisom
    ];
}

2. 集成圖片處理(縮略圖、水印、壓縮)


// 在 processSingleFile 成功移動後調用
if ($result['success'] && str_starts_with($file['type'], 'image/')) {
    $this->generateThumbnail($destPath);
    $this->compressImage($destPath, 85);
}

3. 異步上傳與進度條

前端使用 XMLHttpRequest 或 fetch 配合 FormData,後端返回 JSON,前端輪詢或 WebSocket 接收進度。

安全檢查清單

  • ✅ 禁用腳本執行權限:上傳目錄配置 php_flag engine off (Apache) 或 location ~ \.php$ { deny all; } (Nginx)
  • ✅ 文件名隨機化,防止覆蓋、路徑遍歷
  • ✅ 雙重驗證:MIME + 魔數,防繞過
  • ✅ 大小限制:單文件 + 總量雙重限制
  • ✅ 錯誤信息不洩露服務器路徑
  • ✅ HTTPS 傳輸加密

總結

這個 MultiFileUpload 類採用單一職責原則,將驗證、存儲、命名、統計分離,核心邏輯集中在 processSingleFile(),易於測試、擴展、維護。魔數驗證是防止偽造上傳的關鍵防線,生產環境務必啟用。根據業務需求調整允許類型、大小限制、存儲路徑即可直接投入使用。

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