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

PHP 強制文件下載完整實現:Headers 詳解、大文件支持與安全實踐

     編輯:PHP 相關知識

前言

強制下載(而非瀏覽器預覽)是文件分享、導出報表、附件下載的常見需求。PHP 通過發送特定 HTTP Headers 配合 readfile() 即可實現。本文詳解 Headers 含義、大文件分塊傳輸、內存控制、安全防範及 Nginx/Apache 配置協同。

核心 Headers 詳解

Header值示例作用必要性
Content-DescriptionFile Transfer文件傳輸描述(參考用)可選
Content-Typeapplication/octet-stream通用二進制流,強制瀏覽器下載而非預覽**核心**
Content-Dispositionattachment; filename="report.xlsx"指示為附件,指定下載文件名(需 URL 編碼)**核心**
Content-Transfer-Encodingbinary傳輸編碼為二進制推薦
Expires0過期時間為過去,防止緩存推薦
Cache-Controlmust-revalidate, post-check=0, pre-check=0強制重新驗證,不緩存(HTTP/1.1)推薦
PragmapublicHTTP/1.0 兼容,允許緩存(舊瀏覽器需 public 才能下載)兼容
Content-Length1234567文件字節數,**支持斷點續傳、進度條****核心**

基礎版:小文件直接輸出

<?php
function forceDownload(string $filePath, string $downloadName = ''): void {
    if (!is_file($filePath) || !is_readable($filePath)) {
        http_response_code(404);
        exit('文件不存在或不可讀');
    }
    
    $downloadName = $downloadName ?: basename($filePath);
    $fileSize = filesize($filePath);
    
    // 發送 Headers
    header('Content-Description: File Transfer');
    header('Content-Type: application/octet-stream');
    header('Content-Disposition: attachment; filename="' . rawurlencode($downloadName) . '"');
    header('Content-Transfer-Encoding: binary');
    header('Expires: 0');
    header('Cache-Control: must-revalidate, post-check=0, pre-check=0');
    header('Pragma: public');
    header('Content-Length: ' . $fileSize);
    
    // 清理輸出緩衝,防止內容污染
    ob_clean();
    flush();
    
    // 直接輸出文件內容
    readfile($filePath);
    exit;
}

// 使用
forceDownload('/data/exports/report_20240115.xlsx', '月度報表.xlsx');

進階版:大文件分塊傳輸(節省內存、支持斷點續傳)

function forceDownloadLarge(string $filePath, string $downloadName = ''): void {
    if (!is_file($filePath) || !is_readable($filePath)) {
        http_response_code(404);
        exit('文件不存在');
    }
    
    $downloadName = $downloadName ?: basename($filePath);
    $fileSize = filesize($filePath);
    $chunkSize = 1024 * 1024 * 8; // 8MB 分塊
    
    // 支持 Range 請求(斷點續傳、多線程下載)
    $rangeStart = 0;
    $rangeEnd = $fileSize - 1;
    $httpRange = $_SERVER['HTTP_RANGE'] ?? '';
    
    if ($httpRange) {
        // 解析 Range: bytes=0-1023
        if (preg_match('/bytes=(\d+)-(\d*)/', $httpRange, $matches)) {
            $rangeStart = (int)$matches[1];
            $rangeEnd = $matches[2] !== '' ? (int)$matches[2] : $fileSize - 1;
            if ($rangeStart >= $fileSize || $rangeEnd >= $fileSize) {
                header('HTTP/1.1 416 Range Not Satisfiable');
                header('Content-Range: bytes */' . $fileSize);
                exit;
            }
            header('HTTP/1.1 206 Partial Content');
            header('Content-Range: bytes ' . $rangeStart . '-' . $rangeEnd . '/' . $fileSize);
            $contentLength = $rangeEnd - $rangeStart + 1;
        }
    } else {
        header('HTTP/1.1 200 OK');
        $contentLength = $fileSize;
    }
    
    // 通用 Headers
    header('Content-Type: application/octet-stream');
    header('Content-Disposition: attachment; filename="' . rawurlencode($downloadName) . '"');
    header('Content-Transfer-Encoding: binary');
    header('Accept-Ranges: bytes'); // 聲明支持斷點續傳
    header('Content-Length: ' . $contentLength);
    
    ob_clean();
    flush();
    
    // 分塊讀取輸出
    $handle = fopen($filePath, 'rb');
    fseek($handle, $rangeStart);
    
    $remaining = $contentLength;
    while ($remaining > 0 && !feof($handle) && (connection_status() === CONNECTION_NORMAL)) {
        $readSize = min($chunkSize, $remaining);
        echo fread($handle, $readSize);
        $remaining -= $readSize;
        flush(); // 及時發送到客戶端
    }
    
    fclose($handle);
    exit;
}

安全防範清單

  1. 路徑遍歷防護:basename() + realpath() 確保文件在允許目錄內。
    $baseDir = '/data/downloads/';
    $requested = $_GET['file'] ?? '';
    $realBase = realpath($baseDir);
    $realFile = realpath($baseDir . $requested);
    if (!$realFile || strpos($realFile, $realBase) !== 0) {
        exit('非法文件路徑');
    }
  2. 權限控制:結合業務邏輯(登錄態、角色、訂單歸屬)校驗下載權限。
  3. 文件名編碼:rawurlencode() 防止中文文件名亂碼(RFC 5987 規範更複雜,簡單場景此函數足夠)。
  4. 禁止腳本執行目錄下載**:確保下載目錄不可執行 PHP/腳本。

Web 服務器協同優化

Nginx X-Accel-Redirect(零拷貝、高性能)

// PHP 僅驗證權限,發送內部重定向 Header
header('X-Accel-Redirect: /protected/files/' . $relativePath);
header('Content-Disposition: attachment; filename="' . rawurlencode($name) . '"');
exit;

// Nginx 配置
location /protected/files/ {
    internal;                    # 禁止直接外部訪問
    alias /data/downloads/;      # 真實文件路徑
    sendfile on;                 # 零拷貝
    tcp_nopush on;
    tcp_nodelay on;
}

優勢:PHP 進程瞬間釋放,Nginx 直接以內核態發送文件,支持超大文件、高併發、斷點續傳,無內存壓力。

Apache mod_xsendfile

// PHP
header('X-Sendfile: /data/downloads/' . $relativePath);
// Apache 配置需啟用 mod_xsendfile: XSendFile On, XSendFilePath /data/downloads

常見問題排查

現象可能原因解決
下載文件損壞/打不開輸出前有多餘輸出(空白行、BOM、echo、var_dump)開啟 ob_clean(),確保文件純淨;檢查文件編碼為無 BOM UTF-8
大文件下載中斷/內存耗盡使用 readfile() 或 file_get_contents() 讀入內存改用分塊 fread() 或 Nginx X-Accel-Redirect
中文文件名亂碼Filename 未編碼或編碼不符合瀏覽器預期使用 rawurlencode(),或 RFC 5987 格式:filename*=UTF-8''%E6%96%87%E4%BB%B6.txt
斷點續傳失效未發送 Accept-Ranges: bytes、未處理 Range Header實現 Range 解析與 206 響應
瀏覽器直接顯示而不下載Content-Type 被識別為可預覽類型(如 image/jpeg)強制設置 application/octet-stream + Content-Disposition: attachment

總結

  • **小文件(< 100MB)**readfile() + 完整 Headers 簡單有效。
  • **大文件/高併發**必須分塊傳輸或使用 **Nginx X-Accel-Redirect** 卸載 PHP 進程。
  • **斷點續傳**需實現 Range/Content-Range/206 協議,前端下載工具才能支持多線程/續傳。
  • **安全第一**:路徑校驗、權限控制、目錄隔離、文件名編碼,缺一不可。
  1. 上一篇文章:
  2. 下一篇文章:
Copyright © 程式碼之家 All Rights Reserved