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

PHP 文件上傳完整實戰:從基礎驗證到安全加固

     編輯:PHP 綜合知識

核心流程

PHP 文件上傳涉及 $_FILES 超全局陣列,核心步驟:

  1. 前端 form enctype="multipart/form-data"
  2. PHP 接收 $_FILES['field']
  3. 驗證類型、大小、錯誤碼
  4. 移動臨時文件到目標目錄 move_uploaded_file()

完整上傳處理類

allowedTypes = $config['allowedTypes'] ?? ['image/jpeg', 'image/png', 'image/gif', 'image/webp'];
        $this->maxSize = $config['maxSize'] ?? 5 * 1024 * 1024; // 5MB
        $this->uploadDir = rtrim($config['uploadDir'] ?? __DIR__ . '/uploads', '/') . '/';
        $this->overwrite = $config['overwrite'] ?? false;
        
        if (!is_dir($this->uploadDir)) {
            mkdir($this->uploadDir, 0755, true);
        }
    }
    
    public function upload(string $fieldName): array {
        $this->errors = [];
        
        if (!isset($_FILES[$fieldName])) {
            $this->errors[] = '未找到上傳文件';
            return $this->result(false);
        }
        
        $file = $_FILES[$fieldName];
        
        // 1. 檢查上傳錯誤碼
        if ($file['error'] !== UPLOAD_ERR_OK) {
            $this->errors[] = $this->getErrorMessage($file['error']);
            return $this->result(false);
        }
        
        // 2. 檢查文件大小
        if ($file['size'] > $this->maxSize) {
            $this->errors[] = "文件大小超過限制 ({$this->formatBytes($this->maxSize)})";
            return $this->result(false);
        }
        
        // 3. 檢查 MIME 類型(雙重驗證:瀏覽器回報 + 實際內容檢測)
        $mime = $this->detectMime($file['tmp_name']);
        if (!in_array($mime, $this->allowedTypes, true)) {
            $this->errors[] = "不支援的文件類型: {$mime}";
            return $this->result(false);
        }
        
        // 4. 生成安全文件名
        $ext = $this->getExtension($mime);
        $filename = $this->generateFilename($ext);
        $destPath = $this->uploadDir . $filename;
        
        // 5. 防覆蓋
        if (!$this->overwrite && file_exists($destPath)) {
            $this->errors[] = '文件已存在';
            return $this->result(false);
        }
        
        // 6. 移動文件
        if (!move_uploaded_file($file['tmp_name'], $destPath)) {
            $this->errors[] = '文件移動失敗,請檢查目錄權限';
            return $this->result(false);
        }
        
        // 7. 設置權限(可選)
        chmod($destPath, 0644);
        
        return $this->result(true, [
            'filename' => $filename,
            'path' => $destPath,
            'url' => $this->getPublicUrl($filename),
            'size' => $file['size'],
            'mime' => $mime,
        ]);
    }
    
    // 內容檢測 MIME(防偽裝)
    private function detectMime(string $tmpPath): string {
        $finfo = new finfo(FILEINFO_MIME_TYPE);
        return $finfo->file($tmpPath) ?: 'application/octet-stream';
    }
    
    private function getExtension(string $mime): string {
        $map = [
            'image/jpeg' => 'jpg',
            'image/png'  => 'png',
            'image/gif'  => 'gif',
            'image/webp' => 'webp',
        ];
        return $map[$mime] ?? 'bin';
    }
    
    private function generateFilename(string $ext): string {
        // 日期目錄 + 隨機名,避免單目錄文件過多
        $dateDir = date('Y/m/d');
        $fullDir = $this->uploadDir . $dateDir;
        if (!is_dir($fullDir)) mkdir($fullDir, 0755, true);
        
        $random = bin2hex(random_bytes(16));
        return $dateDir . '/' . $random . '.' . $ext;
    }
    
    private function getPublicUrl(string $filename): string {
        // 根據實際 Web 根目錄調整
        return '/uploads/' . $filename;
    }
    
    private function getErrorMessage(int $code): string {
        $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  => '擴展阻止上傳',
        ];
        return $messages[$code] ?? "未知錯誤代碼: {$code}";
    }
    
    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 result(bool $success, array $data = []): array {
        return array_merge(['success' => $success, 'errors' => $this->errors], $data);
    }
}

// 使用示例
$uploader = new FileUploader([
    'allowedTypes' => ['image/jpeg', 'image/png', 'image/webp'],
    'maxSize' => 10 * 1024 * 1024, // 10MB
    'uploadDir' => __DIR__ . '/public/uploads',
]);

$result = $uploader->upload('avatar');
if ($result['success']) {
    echo "上傳成功: {$result['url']}";
} else {
    echo "上傳失敗: " . implode('; ', $result['errors']);
}

前端表單

支援拖拽上傳

關鍵安全加固清單

風險防範措施
腳本上傳執行上傳目錄禁用 PHP 解析(Nginx: location ~ \.php$ { deny all; })
MIME 偽裝使用 finfo 檢測實際內容,而非信任 $_FILES['type']
文件名注入忽略原文件名,生成隨機名;路徑拼接用 basename
大文件 DoS限制 upload_max_filesize、post_max_size、max_file_uploads
目錄遍歷open_basedir 限制訪問範圍
圖片攜帶惡意代碼重新壓縮/轉換(GD/Imagick)剝離 EXIF 等元數據

圖片處理增強

// 上傳後自動生成縮略圖、壓縮、格式統一
function processImage(string $srcPath, string $destPath, int $maxW = 1200, int $maxH = 1200, int $quality = 85): bool {
    $imgInfo = getimagesize($srcPath);
    if (!$imgInfo) return false;
    
    [$srcW, $srcH, $type] = $imgInfo;
    $mime = image_type_to_mime_type($type);
    
    // 創建源圖像資源
    $createFunc = [
        IMAGETYPE_JPEG => 'imagecreatefromjpeg',
        IMAGETYPE_PNG  => 'imagecreatefrompng',
        IMAGETYPE_GIF  => 'imagecreatefromgif',
        IMAGETYPE_WEBP => 'imagecreatefromwebp',
    ][$type] ?? null;
    
    if (!$createFunc) return false;
    $srcImg = $createFunc($srcPath);
    
    // 計算縮放比例
    $ratio = min($maxW / $srcW, $maxH / $srcH, 1); // 不放大
    $newW = (int)($srcW * $ratio);
    $newH = (int)($srcH * $ratio);
    
    // 創建目標圖像
    $dstImg = imagecreatetruecolor($newW, $newH);
    
    // 保持透明度(PNG/WebP)
    if (in_array($type, [IMAGETYPE_PNG, IMAGETYPE_WEBP])) {
        imagealphablending($dstImg, false);
        imagesavealpha($dstImg, true);
        $transparent = imagecolorallocatealpha($dstImg, 255, 255, 255, 127);
        imagefill($dstImg, 0, 0, $transparent);
    }
    
    // 重採樣縮放
    imagecopyresampled($dstImg, $srcImg, 0, 0, 0, 0, $newW, $newH, $srcW, $srcH);
    
    // 統一輸出為 WebP(現代瀏覽器支援良好,體積更小)
    $result = imagewebp($dstImg, $destPath, $quality);
    
    imagedestroy($srcImg);
    imagedestroy($dstImg);
    
    return $result;
}
  1. 上一篇文章:
  2. 下一篇文章:
Copyright © 程式碼之家 All Rights Reserved