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

PHP 圖片上傳完整解決方案:驗證、縮略圖、水印、安全防護一站式實現

     編輯:PHP 相關知識

上傳流程總覽

企業級圖片上傳需解決:文件驗證、安全存儲、縮略圖生成、水印保護、錯誤處理、性能優化。本文提供生產就緒的完整實現。

核心上傳類設計

<?php
class ImageUploader {
    // 配置屬性
    private string $uploadDir = 'uploads/';
    private int $maxSize = 5 * 1024 * 1024; // 5MB
    private array $allowedTypes = ['jpg', 'jpeg', 'png', 'gif', 'webp'];
    private array $allowedMimes = [
        'image/jpeg', 'image/png', 'image/gif', 'image/webp'
    ];
    
    // 縮略圖配置
    private bool $createThumb = true;
    private int $thumbWidth = 400;
    private int $thumbHeight = 300;
    private string $thumbPrefix = 'thumb_';
    private string $thumbDir = ''; // 空則同目錄
    
    // 水印配置
    private bool $watermark = true;
    private string $wmText = '© MySite.com';
    private string $wmFont = '';
    private int $wmSize = 18;
    private array $wmColor = [255, 255, 255, 60];
    private string $wmPosition = 'bottom-right';
    private string $wmImage = ''; // 圖片水印路徑
    
    // 狀態
    private array $errors = [];
    private string $savedPath = '';
    private string $thumbPath = '';

    public function __construct(array $config = []) {
        foreach ($config as $key => $value) {
            if (property_exists($this, $key)) {
                $this->$key = $value;
            }
        }
        // 確保目錄存在
        $this->ensureDir($this->uploadDir);
        if ($this->thumbDir) $this->ensureDir($this->thumbDir);
    }

    public function upload(array $file): bool {
        $this->errors = [];
        
        // 1. 基礎驗證
        if (!$this->validateUpload($file)) return false;
        
        // 2. 生成安全文件名
        $ext = strtolower(pathinfo($file['name'], PATHINFO_EXTENSION));
        $filename = $this->generateFilename($ext);
        $this->savedPath = $this->uploadDir . $filename;
        
        // 3. 移動文件
        if (!move_uploaded_file($file['tmp_name'], $this->savedPath)) {
            $this->errors[] = '文件移動失敗,請檢查目錄權限';
            return false;
        }
        
        // 4. 二次驗證:檢查文件內容是否真為圖片
        if (!$this->verifyImageContent($this->savedPath)) {
            unlink($this->savedPath);
            $this->errors[] = '文件內容非有效圖片';
            return false;
        }
        
        // 5. 添加水印(先水印再縮略圖,保證縮略圖也有水印)
        if ($this->watermark) {
            $this->applyWatermark($this->savedPath);
        }
        
        // 6. 生成縮略圖
        if ($this->createThumb) {
            $this->createThumbnail($this->savedPath);
        }
        
        return true;
    }

    // ========== 驗證方法 ==========
    private function validateUpload(array $file): bool {
        if ($file['error'] !== UPLOAD_ERR_OK) {
            $this->errors[] = $this->getUploadErrorMsg($file['error']);
            return false;
        }
        
        if ($file['size'] > $this->maxSize) {
            $this->errors[] = "文件大小超過限制 ({$this->formatBytes($this->maxSize)})";
            return false;
        }
        
        $ext = strtolower(pathinfo($file['name'], PATHINFO_EXTENSION));
        if (!in_array($ext, $this->allowedTypes, true)) {
            $this->errors[] = "不支持的文件類型: $ext";
            return false;
        }
        
        $finfo = finfo_open(FILEINFO_MIME_TYPE);
        $mime = finfo_file($finfo, $file['tmp_name']);
        finfo_close($finfo);
        
        if (!in_array($mime, $this->allowedMimes, true)) {
            $this->errors[] = "MIME 類型不匹配: $mime";
            return false;
        }
        
        // 檢查圖片尺寸(防止解壓炸彈)
        [$w, $h] = getimagesize($file['tmp_name']);
        if ($w > 8000 || $h > 8000) {
            $this->errors[] = '圖片尺寸過大';
            return false;
        }
        
        return true;
    }

    private function verifyImageContent(string $path): bool {
        $im = @imagecreatefromstring(file_get_contents($path));
        if (!$im) return false;
        imagedestroy($im);
        return true;
    }

    // ========== 文件名生成 ==========
    private function generateFilename(string $ext): string {
        // 格式:年月日時分秒_隨機字符.擴展名
        $date = date('YmdHis');
        $rand = bin2hex(random_bytes(4));
        return "{$date}_{$rand}.{$ext}";
    }

    // ========== 水印處理 ==========
    private function applyWatermark(string $path): void {
        [$w, $h, $type] = getimagesize($path);
        $createFn = $this->getCreateFn($type);
        $saveFn = $this->getSaveFn($type);
        if (!$createFn || !$saveFn) return;
        
        $im = $createFn($path);
        $this->prepareAlpha($im, $type);
        
        // 文字水印
        if ($this->wmText && $this->wmFont && file_exists($this->wmFont)) {
            $color = imagecolorallocatealpha($im, ...$this->wmColor);
            $box = imagettfbbox($this->wmSize, 0, $this->wmFont, $this->wmText);
            $tw = $box[2] - $box[0];
            $th = $box[1] - $box[7];
            [$x, $y] = $this->calcPosition($w, $h, $tw, $th, $this->wmPosition);
            imagettftext($im, $this->wmSize, 0, $x, $y, $color, $this->wmFont, $this->wmText);
        }
        
        // 圖片水印
        if ($this->wmImage && file_exists($this->wmImage)) {
            [$wmW, $wmH, $wmType] = getimagesize($this->wmImage);
            $wmCreateFn = $this->getCreateFn($wmType);
            if ($wmCreateFn) {
                $wm = $wmCreateFn($this->wmImage);
                $this->prepareAlpha($wm, $wmType);
                [$x, $y] = $this->calcPosition($w, $h, $wmW, $wmH, $this->wmPosition);
                imagealphablending($im, true);
                imagecopymerge($im, $wm, $x, $y, 0, 0, $wmW, $wmH, 50);
                imagedestroy($wm);
            }
        }
        
        $saveFn($im, $path);
        imagedestroy($im);
    }

    // ========== 縮略圖生成 ==========
    private function createThumbnail(string $srcPath): void {
        [$srcW, $srcH, $type] = getimagesize($srcPath);
        $createFn = $this->getCreateFn($type);
        $saveFn = $this->getSaveFn($type);
        if (!$createFn || !$saveFn) return;
        
        // 計算目標尺寸(等比)
        $ratio = min($this->thumbWidth / $srcW, $this->thumbHeight / $srcH);
        if ($ratio >= 1) return; // 源圖已小於目標
        
        $dstW = (int)round($srcW * $ratio);
        $dstH = (int)round($srcH * $ratio);
        
        $srcIm = $createFn($srcPath);
        $dstIm = imagecreatetruecolor($dstW, $dstH);
        $this->prepareAlpha($dstIm, $type);
        
        imagecopyresampled($dstIm, $srcIm, 0, 0, 0, 0, $dstW, $dstH, $srcW, $srcH);
        
        // 保存路徑
        $filename = basename($srcPath);
        $thumbDir = $this->thumbDir ?: $this->uploadDir;
        $this->thumbPath = $thumbDir . $this->thumbPrefix . $filename;
        
        $saveFn($dstIm, $this->thumbPath);
        imagedestroy($srcIm);
        imagedestroy($dstIm);
    }

    // ========== 工具方法 ==========
    private function getCreateFn(int $type): ?callable {
        return match($type) {
            IMAGETYPE_JPEG => 'imagecreatefromjpeg',
            IMAGETYPE_PNG => 'imagecreatefrompng',
            IMAGETYPE_GIF => 'imagecreatefromgif',
            IMAGETYPE_WEBP => 'imagecreatefromwebp',
            default => null
        };
    }
    
    private function getSaveFn(int $type): ?callable {
        return match($type) {
            IMAGETYPE_JPEG => fn($im, $p) => imagejpeg($im, $p, 90),
            IMAGETYPE_PNG => fn($im, $p) => imagepng($im, $p, 9),
            IMAGETYPE_GIF => fn($im, $p) => imagegif($im, $p),
            IMAGETYPE_WEBP => fn($im, $p) => imagewebp($im, $p, 90),
            default => null
        };
    }
    
    private function prepareAlpha($im, int $type): void {
        if (in_array($type, [IMAGETYPE_PNG, IMAGETYPE_GIF, IMAGETYPE_WEBP])) {
            imagealphablending($im, false);
            imagesavealpha($im, true);
        }
    }
    
    private function calcPosition(int $cw, int $ch, int $ow, int $oh, string $pos): array {
        $margin = 15;
        return match($pos) {
            'top-left' => [$margin, $margin + $oh],
            'top-center' => [($cw - $ow)/2, $margin + $oh],
            'top-right' => [$cw - $ow - $margin, $margin + $oh],
            'center-left' => [$margin, ($ch + $oh)/2],
            'center' => [($cw - $ow)/2, ($ch + $oh)/2],
            'center-right' => [$cw - $ow - $margin, ($ch + $oh)/2],
            'bottom-left' => [$margin, $ch - $oh - $margin],
            'bottom-center' => [($cw - $ow)/2, $ch - $oh - $margin],
            'bottom-right' => [$cw - $ow - $margin, $ch - $oh - $margin],
            default => [$cw - $ow - $margin, $ch - $oh - $margin],
        };
    }
    
    private function ensureDir(string $dir): void {
        if (!is_dir($dir)) @mkdir($dir, 0755, true);
    }
    
    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 => '擴展阻止上傳',
            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];
    }

    // ========== 獲取器 ==========
    public function getErrors(): array { return $this->errors; }
    public function getSavedPath(): string { return $this->savedPath; }
    public function getThumbPath(): string { return $this->thumbPath; }
    public function getPublicUrl(string $path): string {
        // 轉換為公開 URL,需根據項目配置調整
        return '/'. ltrim($path, '/');
    }
}

前端表單與後端處理

<!-- HTML 表單 -->
<?php
// upload.php
require 'ImageUploader.php';

header('Content-Type: application/json');

$uploader = new ImageUploader([
    'uploadDir' => __DIR__ . '/uploads/',
    'thumbDir' => __DIR__ . '/uploads/thumbs/',
    'wmFont' => __DIR__ . '/fonts/SourceHanSansCN-Regular.otf',
    'wmText' => $_POST['wm_text'] ?? '© MySite.com',
    'maxSize' => 10 * 1024 * 1024, // 10MB
]);

if ($uploader->upload($_FILES['image'])) {
    echo json_encode([
        'success' => true,
        'url' => $uploader->getPublicUrl($uploader->getSavedPath()),
        'thumb_url' => $uploader->getPublicUrl($uploader->getThumbPath()),
        'message' => '上傳成功'
    ]);
} else {
    http_response_code(400);
    echo json_encode([
        'success' => false,
        'errors' => $uploader->getErrors()
    ]);
}

安全防護清單

風險防護措施實現位置
惡意腳本上傳MIME + 擴展名雙重驗證、imagecreatefromstring 內容驗證validateUpload(), verifyImageContent()
文件名注入/遍歷隨機重命名、basename()、路徑標準化generateFilename()
圖片解壓炸彈限制最大像素尺寸 (8000x8000)getimagesize() 檢查
目錄執行權限上傳目錄配置 php_flag engine off / SetHandler default-handlerNginx/Apache 配置
大文件 DoSMAX_FILE_SIZE、post_max_size、upload_max_filesize、memory_limitphp.ini + 表單隱藏域
並發衝突隨機文件名 + 微秒時間戳generateFilename()

Nginx 上傳目錄安全配置

location /uploads/ {
    # 禁止 PHP 腳本執行
    location ~ \.php$ {
        deny all;
        return 403;
    }
    # 禁止訪問隱藏文件
    location ~ /\. {
        deny all;
    }
    # 緩存靜態資源
    expires 30d;
    add_header Cache-Control "public";
}

擴展:分塊上傳大文件

對於 >100MB 文件,需前端分片 + 後端合併:

<?php
// chunk_upload.php
$chunkDir = 'uploads/chunks/' . $_POST['fileHash'] . '/';
@mkdir($chunkDir, 0755, true);
$chunkFile = $chunkDir . $_POST['chunkIndex'];
move_uploaded_file($_FILES['chunk']['tmp_name'], $chunkFile);

// 合併請求
if ($_POST['isLastChunk']) {
    $finalPath = 'uploads/' . $_POST['fileName'];
    $fp = fopen($finalPath, 'wb');
    for ($i = 0; $i <= $_POST['totalChunks']; $i++) {
        fwrite($fp, file_get_contents($chunkDir . $i));
    }
    fclose($fp);
    // 清理分片
    array_map('unlink', glob($chunkDir . '*'));
    @rmdir($chunkDir);
    // 後續處理:驗證、縮略圖、水印...
}

總結

ImageUploader 類封裝了企業級圖片上傳的完整鏈路:驗證 → 重命名 → 存儲 → 內容驗證 → 水印 → 縮略圖 → 安全輸出。關鍵設計原則:

  1. 雙重驗證:擴展名 + MIME + 內容解析
  2. 隨機命名:避免衝突、遍歷、注入
  3. 處理順序:先水印後縮略圖,保證縮略圖也帶水印
  4. 資源管理:每個 GD 資源必須 imagedestroy()
  5. 配置外置:支持不同業務場景靈活配置

生產部署時務必配合 Web 服務器禁用上傳目錄腳本執行權限,並設置合理的 PHP 上傳限制參數。

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