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

PHP 安全檔案下載實作:中文檔名支援與大檔案傳輸優化

     編輯:PHP 綜合知識

PHP 安全檔案下載完整實作指南

本文提供生產級的 PHP 檔案下載實作,解決中文檔名編碼、大檔案記憶體溢出、斷點續傳、安全性驗證等核心問題。

基礎下載函數

<?php
header('Content-Type: text/html; charset=utf-8');
define('ROOT_PATH', dirname(__FILE__));

/**
 * 安全下載檔案
 * @param string $file_path 絕對路徑
 * @param string $download_name 自訂下載檔名(可選)
 * @param bool $enable_resume 支援斷點續傳
 */
function downFile($file_path, $download_name = '', $enable_resume = true) {
    // 1. 路徑安全檢查:防止目錄遍歷攻擊
    $file_path = realpath($file_path);
    $root_path = realpath(ROOT_PATH);
    if ($file_path === false || strpos($file_path, $root_path) !== 0) {
        exit('非法檔案路徑!');
    }

    // 2. 檔案存在性檢查
    if (!file_exists($file_path) || !is_file($file_path)) {
        exit('檔案不存在!');
    }

    // 3. 讀取權限檢查
    if (!is_readable($file_path)) {
        exit('檔案不可讀!');
    }

    // 4. 取得檔案資訊
    $file_size = filesize($file_path);
    $file_name = $download_name ?: basename($file_path);
    
    // 5. 中文檔名編碼處理(RFC 5987 相容)
    $encoded_name = rawurlencode($file_name);
    $ascii_name = preg_replace('/[^\x20-\x7E]/', '', $file_name); // 移除非 ASCII
    $content_disposition = "attachment; filename=\"$ascii_name\"; filename*=UTF-8''$encoded_name";

    // 6. MIME 類型偵測
    $finfo = finfo_open(FILEINFO_MIME_TYPE);
    $mime_type = finfo_file($finfo, $file_path);
    finfo_close($finfo);
    
    // 預設二進位串流
    if (!$mime_type) $mime_type = 'application/octet-stream';

    // 7. 發送標頭
    header("Content-Type: $mime_type");
    header("Content-Disposition: $content_disposition");
    header("Content-Length: $file_size");
    header("Accept-Ranges: bytes");
    header("Cache-Control: private, must-revalidate");
    header("Pragma: no-cache");

    // 8. 斷點續傳支援
    if ($enable_resume && isset($_SERVER['HTTP_RANGE'])) {
        $range = $_SERVER['HTTP_RANGE'];
        if (preg_match('/bytes=(\d+)-(\d*)/', $range, $matches)) {
            $start = intval($matches[1]);
            $end = $matches[2] ? intval($matches[2]) : $file_size - 1;
            
            if ($start < $file_size && $end < $file_size && $start <= $end) {
                header('HTTP/1.1 206 Partial Content');
                header("Content-Range: bytes $start-$end/$file_size");
                header("Content-Length: " . ($end - $start + 1));
                $file_size = $end - $start + 1;
            }
        }
    }

    // 9. 分塊輸出(避免記憶體溢出)
    $buffer = 1024 * 8; // 8KB 緩衝區
    $fp = fopen($file_path, 'rb');
    
    if (isset($start)) fseek($fp, $start);
    
    while (!feof($fp) && $file_size > 0) {
        $read_size = min($buffer, $file_size);
        $data = fread($fp, $read_size);
        echo $data;
        $file_size -= $read_size;
        
        // 定期 flush 避免緩衝區堆積
        if (ob_get_level()) ob_flush();
        flush();
    }
    
    fclose($fp);
    exit;
}

// 使用範例
downFile(ROOT_PATH . '/down/Sunset.jpg');
?>

進階:下載權限控制與日誌記錄

<?php
function secureDownload($file_id, $user_id) {
    // 1. 資料庫驗證下載權限
    $pdo = Db::getDB();
    $stmt = $pdo->prepare("
        SELECT f.path, f.name, f.size, f.mime, p.permission
        FROM files f
        JOIN permissions p ON f.id = p.file_id
        WHERE f.id = ? AND p.user_id = ? AND p.can_download = 1
    ");
    $stmt->execute([$file_id, $user_id]);
    $file = $stmt->fetch(PDO::FETCH_ASSOC);
    
    if (!$file) {
        // 記錄未授權嘗試
        logDownloadAttempt($user_id, $file_id, 'DENIED');
        exit('無下載權限或檔案不存在');
    }

    // 2. 記錄下載日誌
    logDownloadAttempt($user_id, $file_id, 'ALLOWED', $file['name']);

    // 3. 執行下載
    downFile($file['path'], $file['name']);
}

function logDownloadAttempt($user_id, $file_id, $status, $filename = '') {
    $pdo = Db::getDB();
    $stmt = $pdo->prepare("
        INSERT INTO download_logs (user_id, file_id, filename, status, ip, user_agent, created_at)
        VALUES (?, ?, ?, ?, ?, ?, NOW())
    ");
    $stmt->execute([
        $user_id, $file_id, $filename, $status,
        $_SERVER['REMOTE_ADDR'],
        $_SERVER['HTTP_USER_AGENT']
    ]);
}
?>

大檔案優化策略

問題解決方案
記憶體耗盡分塊讀取(8KB-64KB)、readfile() 替代 file_get_contents()
執行逾時set_time_limit(0)、Nginx proxy_read_timeout 調大
網路中斷HTTP Range 斷點續傳、前端使用 resumable.js
併發限制限流(Token Bucket)、CDN 卸載靜態檔案
磁碟 I/O 瓶頸SSD、RAID、檔案預讀、sendfile 系統呼叫

Nginx X-Accel-Redirect 高效下載(推薦生產環境)

# nginx.conf
location /protected/ {
    internal;                    # 僅允許內部重導向
    alias /var/www/files/;       # 實際檔案目錄
    sendfile on;                 # 核心層傳輸(零拷貝)
    tcp_nopush on;
    aio on;                      # 非同步 I/O
    output_buffers 16 32k;       # 輸出緩衝區
}

# PHP 端
function nginxDownload($file_path, $filename) {
    $relative_path = str_replace('/var/www/files/', '', $file_path);
    header("X-Accel-Redirect: /protected/$relative_path");
    header("Content-Disposition: attachment; filename*=UTF-8''" . rawurlencode($filename));
    header("Content-Type: application/octet-stream");
    exit;
}

安全性檢查清單

  1. 路徑遍歷防護:使用 realpath() 驗證路徑在允許目錄內
  2. 檔案類型驗證:MIME 偵測 + 副檔名白名單
  3. 權限控制:資料庫/ACL 驗證使用者下載授權
  4. 下載日誌:記錄使用者、IP、時間、檔案、狀態
  5. 頻率限制:防止惡意爬蟲/濫用頻寬
  6. 敏感資訊過濾:錯誤訊息不洩露伺服器路徑結構

常見中文檔名問題對照

瀏覽器支援標準建議寫法
Chrome/Firefox/Edge 新版RFC 5987 (filename*)filename*=UTF-8''encoded_name
IE/舊版瀏覽器RFC 2231 部分支援雙重標頭:filename="ascii" + filename*
SafariRFC 5987同 Chrome
  1. 上一篇文章:
  2. 下一篇文章:
Copyright © 程式碼之家 All Rights Reserved