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

PHP 圖片下載功能常見坑:Header 前輸出導致檔案損壞

     編輯:PHP 相關知識

問題現象

實作圖片下載功能時,點擊下載連結後,瀏覽器下載的圖片檔案無法開啟(損壞、大小為 0、或開啟為亂碼),但檔案路徑確認正確、readfile() 邏輯也沒錯。

根本原因

在呼叫 header() 之前已有輸出內容(空白字元、BOM、Echo、錯誤訊息、Include 檔案頭尾的空行等)。HTTP 協定要求 Header 必須在 Body 之前發送,任何提前輸出會導致:

  • Header 發送失敗(Cannot modify header information - headers already sent)
  • 輸出內容混雜在圖片二進制資料中,破壞檔案格式

錯誤代碼範例

<?php
// ❌ 錯誤:這裡有一個換行或空白就會導致輸出
$filename = $_GET["photo"];

// 沒有清除緩衝區
header("Content-Type: image/jpeg");
header("Content-Disposition: attachment; filename=" . $filename);
header("Content-Length: " . filesize($filename));
readfile($filename);

正確解決方案

方法一:ob_clean() 清除輸出緩衝區(推薦)

<?php
$filename = $_GET["photo"];

// 關鍵:清除之前所有輸出緩衝
ob_clean();  

header("Content-Type: image/jpeg");
header("Content-Disposition: attachment; filename=" . basename($filename));
header("Content-Length: " . filesize($filename));
readfile($filename);
exit; // 確保無後續輸出

方法二:ob_start() 在腳本最開頭開啟緩衝

<?php
ob_start(); // 放在檔案最第一行,甚至在任何 HTML/空白之前
// ... 其餘邏輯
header(...);
readfile(...);

方法三:確保檔案無 BOM、無前後空白

  • 使用編輯器儲存為「UTF-8 無 BOM」
  • <?php 緊貼檔案開頭,結尾 ?> 可省略(純 PHP 檔案建議省略)
  • Include/Require 的檔案同樣遵守

完整健壯的下載實作範例

<?php
/**
 * 安全的檔案下載函數
 * @param string $filePath 伺服器絕對路徑
 * @param string $downloadName 下載時顯示的檔名(可選)
 * @param string $mimeType MIME 類型(可選,自動偵測)
 */
function downloadFile($filePath, $downloadName = '', $mimeType = '') {
    // 1. 基礎驗證
    if (!is_file($filePath) || !is_readable($filePath)) {
        http_response_code(404);
        exit('檔案不存在或不可讀');
    }

    // 2. 防止目錄遍歷攻擊
    $realBase = realpath('/var/www/uploads'); // 允許的根目錄
    $realFile = realpath($filePath);
    if (strpos($realFile, $realBase) !== 0) {
        http_response_code(403);
        exit('存取被拒絕');
    }

    // 3. 清除輸出緩衝
    if (ob_get_level()) ob_clean();

    // 4. 設定 Header
    $downloadName = $downloadName ?: basename($filePath);
    // RFC 5987 編碼檔名,支援中文
    $encodedName = rawurlencode($downloadName);
    header("Content-Type: " . ($mimeType ?: mime_content_type($filePath)));
    header("Content-Disposition: attachment; filename*=UTF-8''{$encodedName}");
    header("Content-Length: " . filesize($filePath));
    header("Content-Transfer-Encoding: binary");
    header("Cache-Control: must-revalidate, post-check=0, pre-check=0");
    header("Pragma: public");
    header("Expires: 0");

    // 5. 輸出檔案內容
    $handle = fopen($filePath, 'rb');
    while (!feof($handle) && connection_status() === 0) {
        echo fread($handle, 8192);
        flush();
    }
    fclose($handle);
    exit;
}

// 使用範例
$photo = $_GET['photo'] ?? '';
$safePath = '/var/www/uploads/' . basename($photo); // 簡單防遍歷
downloadFile($safePath);

常見陷阱總結表

現象可能原因解決
下載檔案開頭有 HTML/空白Header 前有 Echo/空白/BOMob_clean() / 移除 BOM / 檔案開頭無空白
中文檔名下載亂碼Content-Disposition 編碼錯誤使用 filename*=UTF-8''+rawurlencode()
大檔案下載中斷/記憶體爆滿readfile() 一次讀入記憶體分塊 fread+flush / X-Sendfile / Nginx X-Accel
IE 下載彈出「無法下載」Cache-Control 衝突 / HTTPS 下 Pragma:no-cache調整 Cache/Pragma Header

結語

ob_clean() 是解決「Header 前輸出」最快的救命稻草,但根治之道是**養成良好習慣**:PHP 檔案無 BOM、純 PHP 檔案省略結尾標籤、入口腳本開頭 ob_start()、錯誤訊息走日誌不直接輸出。下載功能更要注意大檔案分塊傳輸、檔名編碼、路徑安全、MIME 偵測等細節,才能經得起生產環境考驗。

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