直接提供檔案連結 <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;
?>
| 標頭 | 作用 | 關鍵點 |
|---|---|---|
| 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 快取導致下載舊版本或洩露敏感檔 |
// 在輸出檔案前加入
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");
// 完整輸出...
}
若使用 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)PHP 強制下載的核心在於正確的 HTTP 標頭組合。基礎版適合中小檔案;大檔案、高併發場景務必配合 Web Server 的 X-Sendfile 機制,並完善斷點續傳、權限控制、審計日誌,構建企業級檔案分發系統。