基礎強制下載
function force_download(string $filePath, string $filename = ''): void {
if (!is_file($filePath) || !is_readable($filePath)) {
http_response_code(404);
exit('File not found');
}
$filename = $filename ?: basename($filePath);
$size = filesize($filePath);
$mime = mime_content_type($filePath) ?: 'application/octet-stream';
header('Content-Type: ' . $mime);
header('Content-Disposition: attachment; filename="' . rawurlencode($filename) . '"');
header('Content-Length: ' . $size);
header('Accept-Ranges: bytes'); // 支援斷點續傳
header('Cache-Control: private, must-revalidate');
header('Pragma: private');
// 輸出檔案
readfile($filePath);
exit;
}
斷點續傳支援 (HTTP Range)
function force_download_range(string $filePath, string $filename = ''): void {
if (!is_file($filePath)) { http_response_code(404); exit; }
$size = filesize($filePath);
$filename = $filename ?: basename($filePath);
$mime = mime_content_type($filePath) ?: 'application/octet-stream';
$start = 0;
$end = $size - 1;
if (isset($_SERVER['HTTP_RANGE'])) {
$range = $_SERVER['HTTP_RANGE'];
if (preg_match('/bytes=(\d+)-(\d*)/', $range, $m)) {
$start = (int)$m[1];
$end = $m[2] ? (int)$m[2] : $size - 1;
if ($start >= $size || $end >= $size) {
header('HTTP/1.1 416 Range Not Satisfiable');
header('Content-Range: bytes */' . $size);
exit;
}
header('HTTP/1.1 206 Partial Content');
header('Content-Range: bytes ' . $start . '-' . $end . '/' . $size);
}
}
$length = $end - $start + 1;
header('Content-Type: ' . $mime);
header('Content-Disposition: attachment; filename="' . rawurlencode($filename) . '"');
header('Content-Length: ' . $length);
header('Accept-Ranges: bytes');
header('Cache-Control: private');
$fp = fopen($filePath, 'rb');
fseek($fp, $start);
$buffer = 1024 * 8;
while (!feof($fp) && $length > 0) {
$read = min($buffer, $length);
echo fread($fp, $read);
$length -= $read;
}
fclose($fp);
exit;
}
大檔案記憶體優化:X-Sendfile / X-Accel-Redirect
// Nginx 配置
location /protected/ {
internal; # 僅允許內部重定向
alias /var/www/files/;
}
// PHP
function force_download_xsendfile(string $filePath, string $filename = ''): void {
$filename = $filename ?: basename($filePath);
$relPath = '/protected/' . ltrim(str_replace('/var/www/files/', '', $filePath), '/');
header('X-Accel-Redirect: ' . $relPath); // Nginx
// header('X-Sendfile: ' . $filePath); // Apache (需 mod_xsendfile)
header('Content-Type: application/octet-stream');
header('Content-Disposition: attachment; filename="' . rawurlencode($filename) . '"');
exit;
}
安全防禦清單
- 路徑遍歷:
realpath($filePath) 必須在允許目錄內。
- 檔案類型驗證:禁止下載 .php, .phtml, .exe 等可執行檔。
- 權限控制:結合用戶身份、所有權、下載次數限制。
- 防盜鏈:驗證 Referer 或簽名 Token (含過期時間)。
- 速率限制:Nginx
limit_rate 或 PHP 佇列控制。
Content-Disposition 編碼最佳實踐 (RFC 5987)
header('Content-Disposition: attachment; filename="' . rawurlencode($filename) . '"; filename*=UTF-8\'\'' . rawurlencode($filename));