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

PHP 檔案上傳類完整實現:多檔、驗證、重命名、編碼處理與安全防護

     編輯:PHP 相關知識

設計目標

  • 支援單檔/多檔上傳
  • MIME 類型與副檔名雙重驗證
  • 檔案大小限制
  • 自動重命名避免衝突(UUID + 原始副檔名)
  • 中文檔名編碼安全處理
  • 上傳目錄自動建立、權限控制
  • * 錯誤碼標準化、易於國際化

一、核心上傳類

<?php
namespace App\Utils;

class FileUploader {
    // 錯誤碼常數
    public const ERR_OK = 0;
    public const ERR_NO_FILE = 1;
    public const ERR_UPLOAD_FAILED = 2;
    public const ERR_INVALID_TYPE = 3;
    public const ERR_EXCEEDS_SIZE = 4;
    public const ERR_MOVE_FAILED = 5;
    public const ERR_DIR_NOT_WRITABLE = 6;
    
    private string $uploadDir;
    private array $allowedMimes = [];
    private array $allowedExts = [];
    private int $maxSize; // bytes
    private bool $rename = true;
    private int $permissions = 0644;
    
    public function __construct(string $uploadDir, array $options = []) {
        $this->uploadDir = rtrim($uploadDir, '/');
        $this->allowedMimes = $options['mimes'] ?? ['image/jpeg', 'image/png', 'image/gif', 'image/webp', 'application/pdf'];
        $this->allowedExts = $options['exts'] ?? ['jpg', 'jpeg', 'png', 'gif', 'webp', 'pdf'];
        $this->maxSize = ($options['max_size_mb'] ?? 10) * 1024 * 1024;
        $this->rename = $options['rename'] ?? true;
        $this->permissions = $options['permissions'] ?? 0644;
        
        if (!is_dir($this->uploadDir)) {
            mkdir($this->uploadDir, 0755, true);
        }
    }
    
    /**
     * 處理單檔上傳
     * @param array $_FILES['field'] 陣列
     * @return array ['code' => int, 'message' => string, 'data' => array|null]
     */
    public function upload(array $file): array {
        // 1. 基礎檢查
        if (!isset($file['error']) || $file['error'] !== UPLOAD_ERR_OK) {
            return $this->error(self::ERR_NO_FILE, $this->getUploadErrorMsg($file['error'] ?? UPLOAD_ERR_NO_FILE));
        }
        if (!is_uploaded_file($file['tmp_name'])) {
            return $this->error(self::ERR_UPLOAD_FAILED, '非合法上傳檔案');
        }
        
        // 2. 大小檢查
        if ($file['size'] > $this->maxSize) {
            return $this->error(self::ERR_EXCEEDS_SIZE, "檔案大小超過限制 ({$this->formatBytes($this->maxSize)})");
        }
        
        // 3. 類型檢查(MIME + 副檔名)
        $finfo = finfo_open(FILEINFO_MIME_TYPE);
        $mime = finfo_file($finfo, $file['tmp_name']);
        finfo_close($finfo);
        
        $ext = strtolower(pathinfo($file['name'], PATHINFO_EXTENSION));
        if (!in_array($mime, $this->allowedMimes, true) || !in_array($ext, $this->allowedExts, true)) {
            return $this->error(self::ERR_INVALID_TYPE, "不支援的檔案類型: $mime / .$ext");
        }
        
        // 4. 產生儲存檔名
        $filename = $this->rename 
            ? $this->generateSafeName($ext) 
            : $this->sanitizeFilename($file['name']);
        $destPath = $this->uploadDir . '/' . $filename;
        
        // 5. 避免覆蓋(理論上 UUID 不會衝突,雙重保險)
        if (file_exists($destPath)) {
            $filename = $this->generateSafeName($ext);
            $destPath = $this->uploadDir . '/' . $filename;
        }
        
        // 6. 移動檔案
        if (!move_uploaded_file($file['tmp_name'], $destPath)) {
            return $this->error(self::ERR_MOVE_FAILED, '檔案移動失敗,請檢查目錄權限');
        }
        
        // 7. 設定權限
        @chmod($destPath, $this->permissions);
        
        return [
            'code' => self::ERR_OK,
            'message' => '上傳成功',
            'data' => [
                'filename' => $filename,
                'original_name' => $file['name'],
                'path' => $destPath,
                'url' => $this->pathToUrl($destPath),
                'size' => $file['size'],
                'mime' => $mime,
                'ext' => $ext,
            ]
        ];
    }
    
    /**
     * 批次上傳
     * @param array $_FILES['field'] (多檔結構)
     * @return array 每個檔案的結果陣列
     */
    public function uploadMultiple(array $files): array {
        // 正規化多檔陣列結構
        $normalized = $this->normalizeFilesArray($files);
        $results = [];
        foreach ($normalized as $file) {
            $results[] = $this->upload($file);
        }
        return $results;
    }
    
    // ===== 私有輔助方法 =====
    
    private function generateSafeName(string $ext): string {
        return bin2hex(random_bytes(16)) . '.' . $ext; // 32 字元 hex
    }
    
    private function sanitizeFilename(string $name): string {
        // 移除路徑資訊、特殊字元,保留中文
        $name = basename($name);
        $name = preg_replace('/[^\w\-\.\p{Han}]/u', '_', $name);
        return $name;
    }
    
    private function normalizeFilesArray(array $files): array {
        // $_FILES 多檔結構:['name' => [...], 'tmp_name' => [...], ...]
        // 轉為單檔陣列列表
        if (!isset($files['name']) || !is_array($files['name'])) {
            return [$files]; // 單檔
        }
        $count = count($files['name']);
        $normalized = [];
        for ($i = 0; $i < $count; $i++) {
            $normalized[] = [
                'name' => $files['name'][$i],
                'type' => $files['type'][$i],
                'tmp_name' => $files['tmp_name'][$i],
                'error' => $files['error'][$i],
                'size' => $files['size'][$i],
            ];
        }
        return $normalized;
    }
    
    private function getUploadErrorMsg(int $code): string {
        return match($code) {
            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 擴充套件阻擋',
            default => '未知上傳錯誤',
        };
    }
    
    private 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];
    }
    
    private function pathToUrl(string $path): string {
        // 根據專案實際情況實現:返回公開可訪問的 URL
        return '/uploads/' . str_replace($this->uploadDir . '/', '', $path);
    }
    
    private function error(int $code, string $msg): array {
        return ['code' => $code, 'message' => $msg, 'data' => null];
    }
}
?>

二、控制器使用範例

<?php
// Laravel / ThinkPHP / 原生 PHP 通用
$uploader = new \App\Utils\FileUploader(
    __DIR__ . '/../public/uploads',
    [
        'mimes' => ['image/jpeg', 'image/png', 'image/webp'],
        'exts' => ['jpg', 'jpeg', 'png', 'webp'],
        'max_size_mb' => 5,
        'rename' => true,
    ]
);

// 單檔
if (isset($_FILES['avatar'])) {
    $result = $uploader->upload($_FILES['avatar']);
    if ($result['code'] === 0) {
        // 存入資料庫
        $user->avatar = $result['data']['url'];
        $user->save();
        echo json_encode(['success' => true, 'url' => $result['data']['url']]);
    } else {
        http_response_code(400);
        echo json_encode(['success' => false, 'msg' => $result['message']]);
    }
}

// 多檔
if (isset($_FILES['gallery'])) {
    $results = $uploader->uploadMultiple($_FILES['gallery']);
    $success = array_filter($results, fn($r) => $r['code'] === 0);
    echo json_encode(['success' => true, 'files' => $success]);
}
?>

三、前端表單關鍵點

<form action="/upload" method="post" enctype="multipart/form-data">
    <!-- 單檔 -->
    <input type="file" name="avatar" accept="image/jpeg,image/png,image/webp" required>
    
    <!-- 多檔 -->
    <input type="file" name="gallery[]" accept="image/*" multiple>
    
    <button type="submit">上傳</button>
</form>

四、安全防護清單

風險防護措施
惡意腳本上傳MIME 檢測 + 副檔名白名單 + 重命名無副檔名執行權限
大檔案 DoSphp.ini upload_max_filesize, post_max_size + 類內 maxSize
路徑遍歷basename() + 重命名策略
檔案覆蓋UUID 命名 + 存在性檢查
中文檔名亂碼資料庫存原始名,檔案系統用 UUID;或 sanitizeFilename 保留 Unicode
圖片處理漏洞上傳後用 GD/Imagick 重新編碼(imagecreatefromstring -> imagejpeg)

五、進階:圖片二次處理(縮圖、水印、WebP 轉換)

<?php
// 上傳成功後自動生成縮圖
function generateThumbnails(string $sourcePath, array $sizes = [[200,200], [400,400]]): array {
    $results = [];
    foreach ($sizes as [$w, $h]) {
        $dest = preg_replace('/\.(\w+)$/', "_${w}x${h}.\$1", $sourcePath);
        $img = imagecreatefromstring(file_get_contents($sourcePath));
        if ($img) {
            $thumb = imagescale($img, $w, $h, IMG_BICUBIC);
            imagejpeg($thumb, $dest, 85);
            imagedestroy($thumb);
            imagedestroy($img);
            $results[] = $dest;
        }
    }
    return $results;
}

// WebP 轉換(現代瀏覽器優先)
function convertToWebp(string $sourcePath, int $quality = 80): ?string {
    $img = imagecreatefromstring(file_get_contents($sourcePath));
    if (!$img) return null;
    $dest = preg_replace('/\.\w+$/', '.webp', $sourcePath);
    $ok = imagewebp($img, $dest, $quality);
    imagedestroy($img);
    return $ok ? $dest : null;
}
?>

結語

檔案上傳看似簡單,實則涉及安全、編碼、效能、使用者體驗多個維度。本文提供的 FileUploader 類採用「白名單驗證 + UUID 命名 + 權限最小化 + 失敗快速返回」原則,已在多個生產專案驗證。建議根據業務擴充:整合雲存儲、病毒掃描、CDN 刷新等後續流程。

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