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

PHP 文件/圖片批量上傳完整實戰(HTML5 multiple + 服務端驗證)

     編輯:PHP 相關知識

現代批量上傳採用 HTML5 <input type="file" multiple>,前端可預覽、顯示進度;後端遍歷 $_FILES['files']['tmp_name'] 陣列,執行類型/大小/惡意代碼檢測、安全重命名、原子移動。本文提供完整前後端代碼、安全清單、進度條實現方向。

前端:HTML5 原生多文件選擇 + 預覽

<!DOCTYPE html>
<html><head><meta charset="utf-8"><title>批量上傳</title>
<style>
  .preview {display:flex; flex-wrap:wrap; gap:10px; margin-top:10px;}
  .preview img {width:100px; height:100px; object-fit:cover; border:1px solid #ddd;}
  .progress {width:100%; height:20px; background:#eee; margin-top:10px; display:none;}
  .progress-bar {height:100%; background:#007bff; width:0%; transition:width .3s;}
</style></head><body>
<h2>圖片批量上傳</h2>
<form id="uploadForm" enctype="multipart/form-data">
  <input type="file" name="files[]" id="fileInput" multiple accept="image/*" required>
  <div class="preview" id="preview"></div>
  <div class="progress" id="progress"><div class="progress-bar" id="progressBar"></div></div>
  <button type="submit">開始上傳</button>
</form>
<script>
const input = document.getElementById('fileInput');
const preview = document.getElementById('preview');
const form = document.getElementById('uploadForm');
const progress = document.getElementById('progress');
const bar = document.getElementById('progressBar');

// 文件選擇後本地預覽
input.addEventListener('change', () => {
  preview.innerHTML = '';
  [...input.files].forEach(file => {
    if (!file.type.startsWith('image/')) return;
    const url = URL.createObjectURL(file);
    const img = document.createElement('img');
    img.src = url;
    img.title = file.name + ' (' + (file.size/1024).toFixed(1) + ' KB)';
    preview.appendChild(img);
  });
});

// AJAX 提交(支持進度條)
form.addEventListener('submit', async (e) => {
  e.preventDefault();
  if (!input.files.length) return alert('請選擇文件');
  
  const fd = new FormData();
  [...input.files].forEach(f => fd.append('files[]', f));
  
  progress.style.display = 'block';
  bar.style.width = '0%';
  
  try {
    const res = await fetch('upload.php', {
      method: 'POST',
      body: fd,
      // 進度監聽需使用 XMLHttpRequest
    });
    const data = await res.json();
    alert(data.message);
    if (data.success) { input.value = ''; preview.innerHTML = ''; }
  } catch (err) {
    alert('上傳失敗: ' + err.message);
  }
  progress.style.display = 'none';
});

// 真正的進度條需用 XMLHttpRequest
function uploadWithProgress(fd) {
  return new Promise((resolve, reject) => {
    const xhr = new XMLHttpRequest();
    xhr.upload.onprogress = (e) => {
      if (e.lengthComputable) bar.style.width = (e.loaded / e.total * 100) + '%';
    };
    xhr.onload = () => resolve(JSON.parse(xhr.responseText));
    xhr.onerror = () => reject(new Error('Network error'));
    xhr.open('POST', 'upload.php');
    xhr.send(fd);
  });
}
</script>
</body></html>

後端:upload.php 完整驗證與存儲

<?php
// upload.php
header('Content-Type: application/json');

$config = [
    'max_size'      => 5 * 1024 * 1024, // 5MB
    'allowed_types' => ['image/jpeg', 'image/png', 'image/gif', 'image/webp'],
    'allowed_exts'  => ['jpg', 'jpeg', 'png', 'gif', 'webp'],
    'save_dir'      => __DIR__ . '/uploads/',
    'max_files'     => 20,              // 單次最多文件數
];

// 確保目錄存在且不可執行腳本
if (!is_dir($config['save_dir'])) {
    mkdir($config['save_dir'], 0755, true);
    // .htaccess 禁止執行(Apache)
    file_put_contents($config['save_dir'] . '.htaccess', "SetHandler default-handler\nOptions -ExecCGI\nphp_flag engine off");
}

$files = $_FILES['files'] ?? null;
if (!$files || !is_array($files['name'])) {
    echo json_encode(['success' => false, 'message' => '無文件上傳']);
    exit;
}

$count = count($files['name']);
if ($count > $config['max_files']) {
    echo json_encode(['success' => false, 'message' => "單次最多上傳 {$config['max_files']} 個文件"]);
    exit;
}

$uploaded = [];
$errors = [];

for ($i = 0; $i < $count; $i++) {
    $name     = $files['name'][$i];
    $tmpName  = $files['tmp_name'][$i];
    $size     = $files['size'][$i];
    $error    = $files['error'][$i];
    $type     = $files['type'][$i]; // 瀏覽器提供,不可信

    // 1. 上傳錯誤碼檢查
    if ($error !== UPLOAD_ERR_OK) {
        $errors[] = "$name: 上傳錯誤代碼 $error";
        continue;
    }

    // 2. 文件大小
    if ($size > $config['max_size']) {
        $errors[] = "$name: 文件過大 (限制 {$config['max_size']} bytes)";
        continue;
    }

    // 3. MIME 類型驗證(finfo 基於魔數,可靠)
    $finfo = new finfo(FILEINFO_MIME_TYPE);
    $realMime = $finfo->file($tmpName);
    if (!in_array($realMime, $config['allowed_types'], true)) {
        $errors[] = "$name: 類型不允許 ($realMime)";
        continue;
    }

    // 4. 副檔名驗證(雙重保險)
    $ext = strtolower(pathinfo($name, PATHINFO_EXTENSION));
    if (!in_array($ext, $config['allowed_exts'], true)) {
        $errors[] = "$name: 副檔名不允許 ($ext)";
        continue;
    }

    // 5. 圖片內容二次驗證(getimagesize 解析頭部)
    $imgInfo = @getimagesize($tmpName);
    if (!$imgInfo) {
        $errors[] = "$name: 非有效圖片文件";
        continue;
    }

    // 6. 生成安全文件名:隨機 + 時間戳 + 正確副檔名
    $safeName = bin2hex(random_bytes(8)) . '_' . time() . '.' . $ext;
    $destPath = $config['save_dir'] . $safeName;

    // 7. 原子移動(上傳文件已在臨時目錄,move_uploaded_file 檢查是否真為上傳文件)
    if (!move_uploaded_file($tmpName, $destPath)) {
        $errors[] = "$name: 移動失敗";
        continue;
    }

    // 8. 可選:生成縮略圖、記錄數據庫、掃描病毒...
    
    $uploaded[] = [
        'original' => $name,
        'saved'    => $safeName,
        'path'     => $destPath,
        'size'     => $size,
        'mime'     => $realMime,
        'width'    => $imgInfo[0],
        'height'   => $imgInfo[1],
    ];
}

echo json_encode([
    'success' => empty($errors),
    'message' => empty($errors) ? "成功上傳 $count 個文件" : "部分失敗: " . implode('; ', $errors),
    'data'    => $uploaded,
    'errors'  => $errors,
]);

安全加固清單(必做)

風險防禦措施
腳本上傳執行上傳目錄禁用 PHP/腳本執行、副檔名白名單、MIME 驗證、重命名無執行副檔名
文件名注入/遍歷隨機文件名、pathinfo 取副檔名、不信任原文件名
大文件 DoS限制單文件/總大小、Nginx client_max_body_size、PHP post_max_size/upload_max_filesize
圖片炸彈 (解壓巨大)getimagesize 讀取尺寸、限制像素總數 (如 4000x4000)、GD 解碼前檢查
惡意內容隱寫重新壓縮/轉碼、Exif 剝離、ClamAV 掃描
併發競爭隨機文件名幂無衝突、move_uploaded_file 原子性

進階:分片上傳(大文件/斷點續傳)

前端使用 Plupload、Uppy 或自研 Blob.slice() 分片,後端合併分片(需臨時目錄、分片索引、完成標記)。適合 >100MB 視頻、大文件場景。

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