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

PHP 檔案上傳類完整實作:型別驗證、尺寸限制、安全儲存

     編輯:PHP 相關知識

上傳類設計目標

  • 封裝 $_FILES 處理邏輯
  • 支援多種檔案型別驗證 (MIME + 副檔名雙重檢查)
  • 可配置大小限制、儲存目錄、命名規則
  • 防止惡意檔案上傳、路徑遍歷攻擊
  • 提供流暢介面 (Fluent Interface) 鏈式呼叫

完整上傳類實作

<?php
class FileUploader {
    private $file;           // $_FILES 項目
    private $allowedTypes = [];  // 允許的 MIME 型別
    private $allowedExts = [];   // 允許的副檔名
    private $maxSize = 2 * 1024 * 1024; // 預設 2MB
    private $saveDir = 'uploads/';
    private $nameRule = 'hash';    // 'hash' | 'original' | 'timestamp' | callable
    private $overwrite = false;
    private $errors = [];
    
    public function __construct(array $file = null) {
        if ($file) $this->setFile($file);
    }
    
    // 流暢介面設定器
    public function setFile(array $file) { $this->file = $file; return $this; }
    public function allowTypes(array $types) { $this->allowedTypes = $types; return $this; }
    public function allowExts(array $exts) { $this->allowedExts = array_map('strtolower', $exts); return $this; }
    public function maxSize(int $bytes) { $this->maxSize = $bytes; return $this; }
    public function saveTo(string $dir) { $this->saveDir = rtrim($dir, '/') . '/'; return $this; }
    public function nameRule($rule) { $this->nameRule = $rule; return $this; }
    public function allowOverwrite(bool $flag) { $this->overwrite = $flag; return $this; }
    
    // 核心驗證
    private function validate() {
        if (!$this->file || $this->file['error'] !== UPLOAD_ERR_OK) {
            $this->errors[] = '上傳失敗或無檔案: ' . $this->getUploadErrorMsg();
            return false;
        }
        
        // 檔案大小
        if ($this->file['size'] > $this->maxSize) {
            $this->errors[] = sprintf('檔案過大 (%.2f MB),限制 %.2f MB', 
                $this->file['size'] / 1048576, $this->maxSize / 1048576);
        }
        
        // MIME 型別 (需啟用 fileinfo 擴展)
        if ($this->allowedTypes) {
            $finfo = new finfo(FILEINFO_MIME_TYPE);
            $mime = $finfo->file($this->file['tmp_name']);
            if (!in_array($mime, $this->allowedTypes, true)) {
                $this->errors[] = "檔案類型不允許: $mime";
            }
        }
        
        // 副檔名
        if ($this->allowedExts) {
            $ext = strtolower(pathinfo($this->file['name'], PATHINFO_EXTENSION));
            if (!in_array($ext, $this->allowedExts, true)) {
                $this->errors[] = "副檔名不允許: $ext";
            }
        }
        
        // 目錄可寫
        if (!is_dir($this->saveDir)) {
            if (!mkdir($this->saveDir, 0755, true) || !is_writable($this->saveDir)) {
                $this->errors[] = "儲存目錄不可寫: {$this->saveDir}";
            }
        }
        
        return empty($this->errors);
    }
    
    // 產生儲存檔名
    private function generateFilename() {
        $ext = strtolower(pathinfo($this->file['name'], PATHINFO_EXTENSION));
        $ext = $ext ? ".$ext" : '';
        
        switch (true) {
            case $this->nameRule === 'hash':
                return hash_file('sha256', $this->file['tmp_name']) . $ext;
            case $this->nameRule === 'timestamp':
                return date('YmdHis') . '_' . mt_rand(1000,9999) . $ext;
            case $this->nameRule === 'original':
                return basename($this->file['name']);
            case is_callable($this->nameRule):
                return call_user_func($this->nameRule, $this->file) . $ext;
            default:
                return uniqid('up_', true) . $ext;
        }
    }
    
    // 執行上傳
    public function upload() {
        if (!$this->validate()) return false;
        
        $filename = $this->generateFilename();
        $dest = $this->saveDir . $filename;
        
        // 防止覆蓋
        if (!$this->overwrite && file_exists($dest)) {
            $this->errors[] = "檔案已存在: $filename";
            return false;
        }
        
        // 移動檔案 (安全:驗證是真正上傳檔案)
        if (!move_uploaded_file($this->file['tmp_name'], $dest)) {
            $this->errors[] = '檔案移動失敗,請檢查權限';
            return false;
        }
        
        // 設定合理權限
        @chmod($dest, 0644);
        
        return [
            'filename' => $filename,
            'path'     => $dest,
            'url'      => $this->getPublicUrl($filename),
            'size'     => $this->file['size'],
            'mime'     => $this->getMimeType(),
            'ext'      => pathinfo($filename, PATHINFO_EXTENSION)
        ];
    }
    
    public function getErrors() { return $this->errors; }
    public function hasErrors() { return !empty($this->errors); }
    
    private function getUploadErrorMsg() {
        $msgs = [
            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 擴展中斷上傳'
        ];
        return $msgs[$this->file['error']] ?? '未知錯誤';
    }
    
    private function getMimeType() {
        if (function_exists('finfo_open')) {
            $finfo = new finfo(FILEINFO_MIME_TYPE);
            return $finfo->file($this->file['tmp_name']);
        }
        return $this->file['type']; // 瀏覽器提供,不可信
    }
    
    private function getPublicUrl($filename) {
        // 根據實際 Web 根目錄對應調整
        return '/uploads/' . $filename;
    }
}

使用範例

<?php
// 圖片上傳
$uploader = new FileUploader($_FILES['avatar'])
    ->allowTypes(['image/jpeg', 'image/png', 'image/gif', 'image/webp'])
    ->allowExts(['jpg', 'jpeg', 'png', 'gif', 'webp'])
    ->maxSize(5 * 1024 * 1024) // 5MB
    ->saveTo(__DIR__ . '/public/uploads/avatars')
    ->nameRule('hash'); // 內容哈希命名,自動去重

$result = $uploader->upload();
if ($result) {
    echo "上傳成功: {$result['url']}";
    // 儲存 $result['filename'] 到資料庫
} else {
    foreach ($uploader->getErrors() as $err) {
        echo "錯誤: $err\n";
    }
}

// 文件上傳 (自訂命名)
$docUploader = new FileUploader($_FILES['document'])
    ->allowTypes(['application/pdf', 'application/msword', 
                  'application/vnd.openxmlformats-officedocument.wordprocessingml.document'])
    ->nameRule(function($file) {
        return 'doc_' . date('Ymd') . '_' . pathinfo($file['name'], PATHINFO_FILENAME);
    });
?>

安全性關鍵點

  1. MIME 型別用 finfo 驗證,不信任 $_FILES['type']
  2. 副檔名白名單,雙重保險
  3. 儲存目錄非 Web 可執行:Nginx/Apache 設定 location ~ \.php$ { deny all; }
  4. 檔名重命名,防止路徑遍歷、覆蓋、執行惡意腳本
  5. 大小限制雙層:PHP upload_max_filesize + 程式碼邏輯
  1. 上一篇文章:
  2. 下一篇文章:
Copyright © 程式碼之家 All Rights Reserved