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

PHP 強制下載檔案:解決瀏覽器直接開啟圖片/HTML 的完整方案

     編輯:PHP 相關知識

問題場景

直接提供檔案連結 <a href="file.jpg"> 時,瀏覽器會依據 MIME 類型決定行為:圖片、PDF、HTML 等會直接在頁面顯示,而非彈出「另存新檔」對話框。若需強制下載 (如受保護文件、付費素材、日誌匯出),必須由 PHP 讀取檔案並發送正確的 HTTP 標頭。

完整強制下載腳本 download.php

<?php
// 1. 參數驗證 (實際專案請加入權限檢查、防遍歷、Token 驗證)
$fileName = isset($_GET['FileName']) ? basename($_GET['FileName']) : '';
$fileDir  = isset($_GET['FileDir'])  ? rtrim($_GET['FileDir'], '/\\') : '';
$fileId   = isset($_GET['FileId'])   ? $_GET['FileId'] : '';

if (!$fileName || !$fileDir || !$fileId) {
    exit("非法請求:參數不完整");
}

// 2. 安全路徑組合 (防止目錄遍歷 ../)
$fileDir = realpath($fileDir);
if (!$fileDir || strpos($fileDir, realpath(BASE_UPLOAD_DIR)) !== 0) {
    exit("非法路徑");
}
$filePath = $fileDir . DIRECTORY_SEPARATOR . $fileName;

if (!file_exists($filePath) || !is_file($filePath)) {
    exit("檔案不存在");
}

// 3. 發送強制下載標頭
$fileSize = filesize($filePath);

// 關鍵標頭組合
header("Content-Type: application/octet-stream");          // 通用二進制流,瀏覽器不嘗試解析
header("Content-Disposition: attachment; filename=\"{$fileName}\""); // 彈出下載框,建議檔名
header("Content-Length: {$fileSize}");                     // 讓瀏覽器顯示進度條
header("Accept-Ranges: bytes");                            // 支援斷點續傳 (需配合 Range 處理)
header("Cache-Control: private, must-revalidate");         // 私有快取,需重新驗證
header("Pragma: private");                                 // HTTP/1.0 相容
header("Expires: 0");                                      // 立即過期

// 4. 輸出檔案內容 (分塊讀取,避免大檔案佔滿記憶體)
$handle = fopen($filePath, 'rb');
if ($handle) {
    while (!feof($handle) && connection_status() === CONNECTION_NORMAL) {
        echo fread($handle, 8192); // 8KB 緩衝
        flush(); // 立即發送給瀏覽器
    }
    fclose($handle);
}
exit;
?>

HTTP 標頭深度解析

標頭作用關鍵點
Content-Type: application/octet-stream宣告為未知二進制流瀏覽器不會嘗試內嵌顯示,直接觸發下載
Content-Disposition: attachment; filename="..."指示為附件下載並建議檔名檔名含特殊字元需 RFC 5987 編碼 (filename*=utf-8''...)
Content-Length檔案總大小 (位元組)缺少則無進度條、無法斷點續傳
Accept-Ranges: bytes宣告支援範圍請求需額外處理 $_SERVER['HTTP_RANGE'] 實現真正斷點續傳
Cache-Control / Pragma / Expires快取控制避免 Proxy/CDN 快取導致下載舊版本或洩露敏感檔

進階:支援斷點續傳 (HTTP 206 Partial Content)

// 在輸出檔案前加入
if (isset($_SERVER['HTTP_RANGE'])) {
    $range = $_SERVER['HTTP_RANGE']; // bytes=start-end
    preg_match('/bytes=(\d+)-(\d*)/', $range, $m);
    $start = (int)$m[1];
    $end   = isset($m[2]) && $m[2] ? (int)$m[2] : $fileSize - 1;
    $length = $end - $start + 1;

    header("HTTP/1.1 206 Partial Content");
    header("Content-Range: bytes {$start}-{$end}/{$fileSize}");
    header("Content-Length: {$length}");

    fseek($handle, $start);
    // 迴圈讀取 $length 位元組...
} else {
    header("HTTP/1.1 200 OK");
    // 完整輸出...
}

大檔案優化:X-Sendfile / X-Accel-Redirect

若使用 Apache + mod_xsendfile 或 Nginx,PHP 僅驗證權限後發送特殊標頭,由 Web Server 直接以零拷貝 (zero-copy) 高效傳送檔案,PHP 進程瞬間釋放:

// Apache (mod_xsendfile)
header("X-Sendfile: {$filePath}");
header("Content-Type: application/octet-stream");
header("Content-Disposition: attachment; filename=\"{$fileName}\"");
exit;

// Nginx (需在 location 設定 internal 路徑)
header("X-Accel-Redirect: /protected/{$fileName}");
header("Content-Type: application/octet-stream");
header("Content-Disposition: attachment; filename=\"{$fileName}\"");
exit;

安全檢查清單 (上線前必做)

  • ✅ basename() + realpath() 防止目錄遍歷 (../../etc/passwd)
  • ✅ 驗證用戶權限 (資料庫 Token、Session、簽名 URL 時效)
  • ✅ 限制下載頻率/大小/並發 (防盜鏈、防濫用)
  • ✅ 敏感檔案存放於 Web Root 之外
  • ✅ HTTPS 強制傳輸加密
  • ✅ 記錄下載日誌 (審計追蹤)

結語

PHP 強制下載的核心在於正確的 HTTP 標頭組合。基礎版適合中小檔案;大檔案、高併發場景務必配合 Web Server 的 X-Sendfile 機制,並完善斷點續傳、權限控制、審計日誌,構建企業級檔案分發系統。

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