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

PHP 圖片上傳類(含縮略圖生成):完整封裝、錯誤處理、安全校驗

     編輯:PHP 相關知識

前言

圖片上傳是 Web 開發高頻需求。本文提供一個生產級 ImageUploader 類,整合**文件校驗(類型/大小/偽造)、目錄自建、唯一文件名、等比縮略圖生成、異常處理**,代碼結構清晰,可直接納入項目。

核心類代碼

<?php
class ImageUploader {
    // 配置屬性
    private $allowedMimes = ['image/jpeg', 'image/png', 'image/gif', 'image/webp'];
    private $allowedExts  = ['jpg', 'jpeg', 'png', 'gif', 'webp'];
    private $maxSize      = 5 * 1024 * 1024; // 5MB
    private $uploadDir    = '';
    private $thumbWidth   = 200;
    private $thumbHeight  = 200;
    private $thumbPrefix  = 'thumb_';
    private $quality      = 85;

    // 運行時屬性
    private $fileInfo = [];
    private $srcPath  = '';
    private $thumbPath = '';
    private $error    = '';

    public function __construct(array $config = []) {
        foreach ($config as $k => $v) {
            if (property_exists($this, $k)) $this->$k = $v;
        }
    }

    /** 主入口:處理 $_FILES['field'] 上傳
     * @return array|false 成功返回 ['src' => '', 'thumb' => ''],失敗返回 false
     */
    public function upload(string $fieldName): array|false {
        if (empty($_FILES[$fieldName])) {
            $this->error = '未收到文件';
            return false;
        }
        $this->fileInfo = $_FILES[$fieldName];

        if (!$this->validate()) return false;
        if (!$this->prepareDir()) return false;
        if (!$this->moveFile()) return false;
        if (!$this->createThumb()) return false;

        return [
            'src'   => $this->srcPath,
            'thumb' => $this->thumbPath,
        ];
    }

    /** 校驗:錯誤碼、MIME、擴展名、大小、偽造文件 */
    private function validate(): bool {
        $err = $this->fileInfo['error'];
        if ($err !== UPLOAD_ERR_OK) {
            $this->error = match ($err) {
                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 => '未知上傳錯誤',
            };
            return false;
        }

        $finfo = finfo_open(FILEINFO_MIME_TYPE);
        $mime = finfo_file($finfo, $this->fileInfo['tmp_name']);
        finfo_close($finfo);
        if (!in_array($mime, $this->allowedMimes, true)) {
            $this->error = "文件類型不允許: $mime";
            return false;
        }

        $ext = strtolower(pathinfo($this->fileInfo['name'], PATHINFO_EXTENSION));
        if (!in_array($ext, $this->allowedExts, true)) {
            $this->error = "擴展名不允許: $ext";
            return false;
        }

        if ($this->fileInfo['size'] > $this->maxSize) {
            $this->error = '文件過大';
            return false;
        }

        if (!is_uploaded_file($this->fileInfo['tmp_name'])) {
            $this->error = '偽造上傳文件';
            return false;
        }
        return true;
    }

    /** 確保上傳目錄存在 */
    private function prepareDir(): bool {
        if (!is_dir($this->uploadDir)) {
            if (!mkdir($this->uploadDir, 0755, true)) {
                $this->error = '無法創建上傳目錄';
                return false;
            }
        }
        return is_writable($this->uploadDir);
    }

    /** 移動文件,生成唯一文件名 */
    private function moveFile(): bool {
        $ext = strtolower(pathinfo($this->fileInfo['name'], PATHINFO_EXTENSION));
        $filename = bin2hex(random_bytes(16)) . '.' . $ext; // 32位隨機十六進制
        $this->srcPath = $this->uploadDir . '/' . $filename;

        if (!move_uploaded_file($this->fileInfo['tmp_name'], $this->srcPath)) {
            $this->error = '文件移動失敗';
            return false;
        }
        // 修正權限
        chmod($this->srcPath, 0644);
        return true;
    }

    /** 生成等比縮略圖 */
    private function createThumb(): bool {
        list($srcW, $srcH, $type) = getimagesize($this->srcPath);
        if (!$srcW || !$srcH) {
            $this->error = '無法讀取圖片尺寸';
            return false;
        }

        // 計算目標尺寸(等比縮放,限制在矩形框內)
        $ratio = min($this->thumbWidth / $srcW, $this->thumbHeight / $srcH);
        $dstW = (int)($srcW * $ratio);
        $dstH = (int)($srcH * $ratio);

        $srcImg = match ($type) {
            IMAGETYPE_JPEG => imagecreatefromjpeg($this->srcPath),
            IMAGETYPE_PNG  => imagecreatefrompng($this->srcPath),
            IMAGETYPE_GIF  => imagecreatefromgif($this->srcPath),
            IMAGETYPE_WEBP => imagecreatefromwebp($this->srcPath),
            default => null,
        };
        if (!$srcImg) {
            $this->error = '不支持的圖片格式';
            return false;
        }

        $thumbImg = imagecreatetruecolor($dstW, $dstH);
        // PNG/GIF/WEBP 保持透明度
        if (in_array($type, [IMAGETYPE_PNG, IMAGETYPE_GIF, IMAGETYPE_WEBP])) {
            imagealphablending($thumbImg, false);
            imagesavealpha($thumbImg, true);
            $transparent = imagecolorallocatealpha($thumbImg, 255, 255, 255, 127);
            imagefilledrectangle($thumbImg, 0, 0, $dstW, $dstH, $transparent);
        }

        imagecopyresampled($thumbImg, $srcImg, 0, 0, 0, 0, $dstW, $dstH, $srcW, $srcH);

        $thumbFilename = $this->thumbPrefix . basename($this->srcPath);
        $this->thumbPath = $this->uploadDir . '/' . $thumbFilename;

        $success = match ($type) {
            IMAGETYPE_JPEG => imagejpeg($thumbImg, $this->thumbPath, $this->quality),
            IMAGETYPE_PNG  => imagepng($thumbImg, $this->thumbPath, 9),
            IMAGETYPE_GIF  => imagegif($thumbImg, $this->thumbPath),
            IMAGETYPE_WEBP => imagewebp($thumbImg, $this->thumbPath, $this->quality),
            default => false,
        };

        imagedestroy($srcImg);
        imagedestroy($thumbImg);

        if (!$success) {
            $this->error = '縮略圖生成失敗';
            return false;
        }
        chmod($this->thumbPath, 0644);
        return true;
    }

    public function getError(): string { return $this->error; }
}
?>

使用示例

<?php
require 'ImageUploader.php';

$uploader = new ImageUploader([
    'uploadDir' => __DIR__ . '/uploads',
    'maxSize'   => 10 * 1024 * 1024, // 10MB
    'thumbWidth'  => 300,
    'thumbHeight' => 300,
]);

if ($_SERVER['REQUEST_METHOD'] === 'POST') {
    $result = $uploader->upload('avatar'); // 
    if ($result) {
        echo json_encode(['code' => 0, 'data' => $result]);
    } else {
        echo json_encode(['code' => 1, 'msg' => $uploader->getError()]);
    }
    exit;
}
?>
<form method="post" enctype="multipart/form-data">
    <input type="file" name="avatar" accept="image/*" required>
    <button type="submit">上傳</button>
</form>

安全加固清單

措施實現位置說明
MIME 類型白名單finfo_file()防止偽造擴展名繞過檢查
文件大小限制$maxSize + UPLOAD_ERR_INI_SIZE雙重保障(php.ini + 代碼)
隨機文件名bin2hex(random_bytes(16))防止覆蓋、遍歷、執行腳本
上傳目錄非 Web 可執行Nginx: location ~ \.php$ { deny all; }即使上傳 .php 也無法執行
禁用腳本執行權限chmod 0644去除可執行位
圖片重繪驗證imagecreatefrom* + imagecopyresampled破壞隱藏在圖片中的惡意代碼
病毒掃描ClamAV 集成高安全場景額外增加

常見問題排查

  1. 縮略圖黑邊/失真:檢查 imagealphablending/imagesavealpha 設置,PNG/GIF/WEBP 需開啟透明保護
  2. 大圖內存溢出:GD 解碼需 寬×高×4 字節 內存,5000×5000 約 100MB。大圖建議用 Imagick 流式處理或分塊縮放
  3. 權限錯誤:確保 uploadDir 屬於 PHP-FPM 用戶(chown www:www uploads)
  4. 動畫 GIF/WebP 只取第一幀:GD 不支持多幀,需用 Imagick 保持動畫

結語

ImageUploader 類將「校驗、存儲、縮略、安全」四大核心職責封裝於單一類中,配置靈活、錯誤信息友好、擴展性強。生產環境建議配合 雲存儲 SDK(OSS/COS/S3) 替換本地存儲邏輯,並接入 異步隊列 處理耗時的縮略圖生成、水印添加、病毒掃描等任務。

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