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

PHP 中文文件名下載亂碼徹底解決:RFC 5987 標準與瀏覽器兼容方案

     編輯:PHP 相關知識

問題現象

  • 下載文件彈窗顯示亂碼文件名(如 涓婂崍璧勬簮.txt)
  • 不同瀏覽器/版本表現不一(Chrome/Firefox/Edge/Safari/IE)
  • 中文文件名包含空格、特殊字符時更嚴重

根因

HTTP/1.1 RFC 2616 規定 Content-Disposition 的 filename 參數僅支持 ISO-8859-1 (Latin-1)。瀏覽器為兼容歷史各自實現了不同的「擴展解析邏輯」:

  • Chrome/Firefox/Edge:支持 RFC 5987 filename*=UTF-8''encoded_name
  • 舊版 IE/舊版 Safari:仅支持 filename="gbk_encoded_name" 或自動檢測
  • 部分舊庫:雙重發送 filename + filename*

標準解決方案:雙重 Header 兼容所有瀏覽器

<?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''...備註
Chrome50+✅ 完美優先使用 filename*
Firefox50+✅ 完美優先使用 filename*
Edge79+ (Chromium)✅ 完美同 Chrome
Safari13+✅ 完美支持 RFC 5987
IE11❌ 不支持需 filename=GBK編碼,現代項目建議放棄
舊版移動端 WebView-⚠️ 部分支持建議 App 端原生下載

極致兼容方案(含 IE11 支持)

<?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>

常見錯誤避坑

  1. 手動拼接 Content-Disposition 未編碼:header("Content-Disposition: attachment; filename=$name") → 中文直接進 Header → 亂碼/截斷。
  2. 使用 urlencode() 而非 rawurlencode():urlencode 將空格編碼為 +,RFC 5987 要求 %20。
  3. 缺少 Content-Length:導致無法顯示下載進度、無法斷點續傳。
  4. 輸出前有垃圾數據:未 ob_clean(),導致文件內容損壞(參考「PHP 下載 JPG 為空」一文)。
  5. Nginx 代理緩衝干擾:大文件需關閉 proxy_buffering off; 或設置 X-Accel-Redirect 內部重定向。

生產級 Nginx + PHP 大文件下載優化(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; }

總結

  1. 現代項目:僅發送 filename*=UTF-8''rawurlencode(name),完美支持 Chrome/Firefox/Edge/Safari。
  2. 需兼容 IE11:雙重發送 filename*=UTF-8''... + filename="GBK編碼"。
  3. 核心函數:rawurlencode()(非 urlencode()),readfile()(流式輸出),ob_clean()(清理緩衝)。
  4. 大文件/高併發:使用 Nginx X-Accel-Redirect 卸載 PHP 進程。
  1. 上一篇文章:
  2. 下一篇文章:
Copyright © 程式碼之家 All Rights Reserved