標準三步驟
<?php
// 1. 宣告 MIME 類型
header('Content-Type: application/octet-stream');
// 2. 指定下載檔名 (RFC 5987 編碼支援中文)
$filename = '報表.xlsx';
header('Content-Disposition: attachment; filename="' . rawurlencode($filename) . '"');
header("Content-Disposition: attachment; filename*=UTF-8''" . rawurlencode($filename));
// 3. 輸出檔案內容
readfile('/path/to/' . $filename);
exit; // 關鍵:阻止後續輸出混入
?>
常見坑與解法
| 問題 | 原因 | 解決 |
| 下載檔案開頭有 HTML/空白/錯誤訊息 | 腳本前有輸出 (BOM、echo、錯誤、空白行) | 腳本最開頭 <?php 無前綴字元;關閉錯誤顯示 ini_set('display_errors',0);或用 ob_clean() 清緩衝 |
| 中文檔名亂碼 | 瀏覽器編碼解析不一 | 雙重 header:filename (舊瀏覽器) + filename*=UTF-8'' (RFC 5987) |
| 大檔案記憶體耗盡 | readfile 仍需載入內核緩衝 | 分塊輸出:fopen + fread 迴圈 + flush() |
| 斷點續傳支援 | 無 Range 處理 | 解析 $_SERVER['HTTP_RANGE'],回應 206 Partial Content |
大檔案分塊輸出範例
<?php
function downloadLargeFile($path, $filename)
{
if (!is_file($path)) { http_response_code(404); exit; }
$size = filesize($path);
$fp = fopen($path, 'rb');
header('Content-Type: application/octet-stream');
header('Content-Disposition: attachment; filename="' . rawurlencode($filename) . '"');
header('Content-Length: ' . $size);
header('Accept-Ranges: bytes');
header('Cache-Control: no-cache, must-revalidate');
header('Pragma: no-cache');
// Range 支援
if (isset($_SERVER['HTTP_RANGE'])) {
// 解析 Range header,送 206...
}
while (!feof($fp) && connection_status() === CONNECTION_NORMAL) {
echo fread($fp, 8192); // 8KB 分塊
flush();
}
fclose($fp);
exit;
}
?>
安全性檢查清單
- ✅ 驗證路徑在允許目錄內 (
realpath + 前綴比對)
- ✅ 檢查權限 (登入狀態、擁有者、下載次數限制)
- ✅ 記錄下載日誌 (審計追蹤)
- ✅ 設定
X-Content-Type-Options: nosniff 防 MIME 嗅探
- 上一篇文章: PHP 字母順序反轉但單詞順序不變:字串處理面試題實作
- 下一篇文章: PHP 抓取遠端圖片儲存本地:GrabImage 函數完整實作與批次下載範例
Copyright ©
程式碼之家 All Rights Reserved