涓婂崍璧勬簮.txt)HTTP/1.1 RFC 2616 規定 Content-Disposition 的 filename 參數僅支持 ISO-8859-1 (Latin-1)。瀏覽器為兼容歷史各自實現了不同的「擴展解析邏輯」:
filename*=UTF-8''encoded_namefilename="gbk_encoded_name" 或自動檢測filename + filename*<?php
/**
* 安全發送文件下載,完美支持中文文件名
* @param string $filePath 服務器絕對路徑
* @param string $originalName 原始文件名(含中文)
* @param string $mimeType MIME 類型(可選,自動檢測)
*/
function safeDownload($filePath, $originalName, $mimeType = '') {
if (!is_file($filePath) || !is_readable($filePath)) {
header('HTTP/1.1 404 Not Found');
exit('File not found');
}
$fileSize = filesize($filePath);
$mimeType = $mimeType ?: (function_exists('mime_content_type') ? mime_content_type($filePath) : 'application/octet-stream');
// ========== 核心:RFC 5987 編碼 ==========
// 格式:filename*=UTF-8''urlencoded_name
$encodedName = rawurlencode($originalName); // RFC 3986 編碼,空格→%20
$contentDisposition = "attachment; filename*=UTF-8''{$encodedName}";
// 兼容舊瀏覽器:同時提供 filename 參數(UTF-8 直接輸出,現代瀏覽器會忽略它優先用 filename*)
// 注意:舊 IE 需要 GBK 編碼,但現代項目通常可放棄 IE 支持
// 如需極致兼容,可雙重發送:
// $contentDisposition .= '; filename="' . rawurlencode($originalName) . '"'; // 現代瀏覽器忽略
// ========== 標準下載 Headers ==========
header('Content-Type: ' . $mimeType);
header('Content-Length: ' . $fileSize);
header('Content-Transfer-Encoding: binary');
header('Content-Disposition: ' . $contentDisposition);
header('Expires: 0');
header('Cache-Control: must-revalidate, post-check=0, pre-check=0');
header('Pragma: public'); // HTTP/1.0 兼容
// ========== 清理緩衝區並輸出 ==========
if (ob_get_level()) ob_clean();
flush();
readfile($filePath);
exit;
}
// ========== 使用示例 ==========
$filePath = '/data/uploads/2023/08/報表_20230815.xlsx';
$originalName = '銷售報表_20230815.xlsx';
safeDownload($filePath, $originalName, 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet');
?>
| 瀏覽器 | 版本 | filename*=UTF-8''... | 備註 |
|---|---|---|---|
| Chrome | 50+ | ✅ 完美 | 優先使用 filename* |
| Firefox | 50+ | ✅ 完美 | 優先使用 filename* |
| Edge | 79+ (Chromium) | ✅ 完美 | 同 Chrome |
| Safari | 13+ | ✅ 完美 | 支持 RFC 5987 |
| IE | 11 | ❌ 不支持 | 需 filename=GBK編碼,現代項目建議放棄 |
| 舊版移動端 WebView | - | ⚠️ 部分支持 | 建議 App 端原生下載 |
<?php
function safeDownloadLegacy($filePath, $originalName, $mimeType = '') {
// ... 前置檢查同前 ...
$encodedUtf8 = rawurlencode($originalName);
$encodedGbk = rawurlencode(mb_convert_encoding($originalName, 'GBK', 'UTF-8'));
// 雙重 filename:現代瀏覽器用 filename*,舊 IE 用 filename (GBK)
// 注意:filename 參數值建議用 ASCII 字符,GBK 編碼後再 urlencode
$disposition = "attachment; filename*=UTF-8''{$encodedUtf8}; filename=\"{$encodedGbk}\"";
header('Content-Disposition: ' . $disposition);
// ... 其餘 headers 同前 ...
}
?>
<!-- 1. 直接鏈接(最簡單,SEO 友好) -->
<a href="/download.php?file=report_2023.xlsx" download>下載報表</a>
<!-- 2. AJAX 獲取 Blob 再下載(需後端支持 CORS/認證) -->
<script>
async function downloadFile(url, filename) {
const response = await fetch(url, { credentials: 'include' });
const blob = await response.blob();
const link = document.createElement('a');
link.href = URL.createObjectURL(blob);
link.download = filename; // 瀏覽器彈窗將顯示此文件名
link.click();
URL.revokeObjectURL(link.href);
}
</script>
Content-Disposition 未編碼:header("Content-Disposition: attachment; filename=$name") → 中文直接進 Header → 亂碼/截斷。urlencode() 而非 rawurlencode():urlencode 將空格編碼為 +,RFC 5987 要求 %20。Content-Length:導致無法顯示下載進度、無法斷點續傳。ob_clean(),導致文件內容損壞(參考「PHP 下載 JPG 為空」一文)。proxy_buffering off; 或設置 X-Accel-Redirect 內部重定向。<?php
// PHP 僅驗證權限、生成內部重定向 Header,不讀取文件內容
function xAccelDownload($internalPath, $originalName) {
// $internalPath: Nginx 內部路徑,如 /protected/files/report.xlsx
// 需 Nginx 配置 internal location
header('Content-Type: application/octet-stream');
header('Content-Disposition: attachment; filename*=UTF-8\'\'' . rawurlencode($originalName));
header('X-Accel-Redirect: ' . $internalPath); // 關鍵:交給 Nginx 發送文件
header('X-Accel-Buffering: no');
exit;
}
?>
Nginx 配置:
location /protected/files/ {
internal; # 只允許內部重定向訪問
alias /data/uploads/; # 實際文件存儲路徑
add_header Content-Disposition ''; # 清除 PHP 設置的,避免雙重
}
# 下載端點 location /download/ { fastcgi_pass php; }
filename*=UTF-8''rawurlencode(name),完美支持 Chrome/Firefox/Edge/Safari。filename*=UTF-8''... + filename="GBK編碼"。rawurlencode()(非 urlencode()),readfile()(流式輸出),ob_clean()(清理緩衝)。X-Accel-Redirect 卸載 PHP 進程。