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

PHP 檔案上傳與下載完整實作:安全、分頁、多檔案支援

     編輯:PHP 相關知識

PHP 檔案處理核心功能實戰指南

檔案上傳下載是 Web 系統的基礎功能,但也是安全漏洞高發區。本文提供符合現代標準的完整實作,涵蓋單檔/多檔上傳、檔案類型驗證、安全命名、斷點下載與權限控制。

一、安全檔案上傳實作

前端表單 (upload.html)

<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8">
    <title>檔案上傳</title>
</head>
<body>
<form action="upload.php" method="post" enctype="multipart/form-data">
    <label>用戶名:<input type="text" name="username" required></label><br>
    <label>檔案說明:<br><textarea name="fileintro" rows="4" cols="50"></textarea></label><br>
    <label>選擇檔案:<input type="file" name="myfile" required></label><br>
    <button type="submit">上傳檔案</button>
</form>
</body>
</html>

後端處理 (upload.php)

<?php
declare(strict_types=1);

// 設定上傳根目錄 (建議置於 Web 根目錄外)
define('UPLOAD_ROOT', __DIR__ . '/../data/uploads');

// 允許的 MIME 類型
const ALLOWED_TYPES = [
    'image/jpeg', 'image/png', 'image/gif', 'image/webp',
    'application/pdf',
    'application/msword',
    'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
];

// 最大檔案大小 (5MB)
const MAX_FILE_SIZE = 5 * 1024 * 1024;

session_start();
header('Content-Type: application/json; charset=utf-8');

$response = ['success' => false, 'message' => '', 'data' => null];

// 驗證登入狀態 (示例)
if (empty($_SESSION['user_id'])) {
    $response['message'] = '請先登入';
    echo json_encode($response);
    exit;
}

$username = trim($_POST['username'] ?? '');
$intro    = trim($_POST['fileintro'] ?? '');

if ($username === '') {
    $response['message'] = '用戶名不可為空';
    echo json_encode($response);
    exit;
}

// 檢查檔案上傳狀態
if (!isset($_FILES['myfile']) || $_FILES['myfile']['error'] !== UPLOAD_ERR_OK) {
    $response['message'] = '檔案上傳失敗,錯誤代碼: ' . ($_FILES['myfile']['error'] ?? 'unknown');
    echo json_encode($response);
    exit;
}

$file = $_FILES['myfile'];

// 檔案大小驗證
if ($file['size'] > MAX_FILE_SIZE) {
    $response['message'] = '檔案過大,限制 ' . (MAX_FILE_SIZE / 1024 / 1024) . 'MB';
    echo json_encode($response);
    exit;
}

// MIME 類型驗證 (需安裝 fileinfo 擴展)
$finfo = new finfo(FILEINFO_MIME_TYPE);
$mime  = $finfo->file($file['tmp_name']);
if (!in_array($mime, ALLOWED_TYPES, true)) {
    $response['message'] = '不允許的檔案類型: ' . $mime;
    echo json_encode($response);
    exit;
}

// 建立用戶專屬目錄
$userDir = UPLOAD_ROOT . '/' . $username;
if (!is_dir($userDir)) {
    if (!mkdir($userDir, 0755, true)) {
        $response['message'] = '無法建立上傳目錄';
        echo json_encode($response);
        exit;
    }
}

// 產生安全檔名:時間戳 + 隨機字串 + 原始副檔名
$ext = pathinfo($file['name'], PATHINFO_EXTENSION);
$safeName = date('YmdHis') . '_' . bin2hex(random_bytes(8)) . ($ext ? '.' . $ext : '');
$destPath = $userDir . '/' . $safeName;

// 移動檔案
if (!move_uploaded_file($file['tmp_name'], $destPath)) {
    $response['message'] = '檔案儲存失敗';
    echo json_encode($response);
    exit;
}

// 記錄檔案資訊至資料庫 (略)
// $db->insert('uploads', [...]);

$response['success'] = true;
$response['message'] = '上傳成功';
$response['data'] = [
    'original_name' => $file['name'],
    'saved_name'    => $safeName,
    'size'          => $file['size'],
    'mime'          => $mime,
    'path'          => $destPath,
];

echo json_encode($response);

二、檔案下載與斷點續傳支援

<?php
// download.php
declare(strict_types=1);

$filePath = __DIR__ . '/../data/uploads/' . ($_GET['user'] ?? '') . '/' . ($_GET['file'] ?? '');

// 安全性檢查:防止目錄遍歷
$realBase = realpath(__DIR__ . '/../data/uploads');
$realFile = realpath($filePath);

if (!$realFile || strpos($realFile, $realBase) !== 0 || !is_file($realFile)) {
    http_response_code(404);
    exit('檔案不存在或存取被拒絕');
}

// 取得檔案資訊
$fileSize = filesize($realFile);
$fileName = basename($realFile);
$mimeType = mime_content_type($realFile);

// 處理 Range 標頭 (斷點續傳)
$rangeHeader = $_SERVER['HTTP_RANGE'] ?? '';
$start = 0;
$end = $fileSize - 1;
$statusCode = 200;

if (preg_match('/bytes=(\d+)-(\d*)/', $rangeHeader, $matches)) {
    $start = (int)$matches[1];
    $end = $matches[2] !== '' ? min((int)$matches[2], $fileSize - 1) : $fileSize - 1;
    if ($start > $end || $start >= $fileSize) {
        header('Content-Range: bytes */' . $fileSize);
        http_response_code(416);
        exit;
    }
    $statusCode = 206;
    header('Content-Range: bytes ' . $start . '-' . $end . '/' . $fileSize);
}

// 設定回應標頭
header('Content-Type: ' . $mimeType);
header('Content-Length: ' . ($end - $start + 1));
header('Accept-Ranges: bytes');
header('Content-Disposition: attachment; filename="' . rawurlencode($fileName) . '"');
http_response_code($statusCode);

// 串流輸出 (節省記憶體)
$fp = fopen($realFile, 'rb');
fseek($fp, $start);
$bufferSize = 8192;
while (!feof($fp) && ($pos = ftell($fp)) <= $end) {
    $readSize = min($bufferSize, $end - $pos + 1);
    echo fread($fp, $readSize);
    flush(); // 即時發送給客戶端
}
fclose($fp);

三、多檔案列表與下載頁面

<?php
// list.php
$uploadDir = __DIR__ . '/../data/uploads';
$users = array_filter(glob($uploadDir . '/*'), 'is_dir');
?>
<!DOCTYPE html>
<html>
<head><meta charset="utf-8"><title>檔案列表</title></head>
<body>
<h1>用戶檔案列表</h1>
<?php foreach ($users as $userDir): ?>
    <h2><?= basename($userDir) ?></h2>
    <ul>
    <?php foreach (glob($userDir . '/*') as $file): ?>
        <li>
            <a href="download.php?user=<?= urlencode(basename($userDir)) ?>&file=<?= urlencode(basename($file)) ?>">
                <?= htmlspecialchars(basename($file)) ?> (<?= round(filesize($file)/1024, 1) ?> KB)
            </a>
        </li>
    <?php endforeach; ?>
    </ul>
<?php endforeach; ?>
</body>
</html>

安全性關鍵點總結

  1. MIME 類型白名單:使用 finfo 偵測真實類型,不要信任 $_FILES['type']。
  2. 檔名重命名:移除原始檔名,防止路徑遍歷、覆蓋、特殊字元問題。
  3. 儲存路徑隔離:上傳目錄置於 Web Root 外,避免直接 URL 存取執行腳本。
  4. 權限控制:下載前驗證用戶身份與檔案擁有權。
  5. 大小限制:同時在 PHP (upload_max_filesize)、Nginx/Apache (client_max_body_size) 設定。
  6. 錯誤處理:統一 JSON 回應,避免洩露伺服器路徑等敏感資訊。
  1. 上一篇文章:
  2. 下一篇文章:
Copyright © 程式碼之家 All Rights Reserved