php强制下载文件必须同时设置content-type、content-disposition和content-length三组header;中文文件名需用filename*=utf-8''+rawurlencode()编码;大文件须用readfile()流式输出并校验路径与可读性。

PHP 用 header 强制下载文件,关键在三组 header
直接结论:必须同时设置 Content-Type、Content-Disposition 和 Content-Length,缺一不可,否则浏览器可能打开而非下载,或中断传输。
常见错误现象是文件名乱码、下载后打不开、进度卡在 99%、或直接在浏览器里渲染(比如 PDF 显示成页面)。根本原因是 HTTP 响应头没告诉浏览器“这是个要保存的二进制文件”,而默认行为是解析并展示。
-
Content-Type: application/octet-stream最稳妥,避免 MIME 类型被识别成 text/html 或 image/png 导致内嵌显示 -
Content-Disposition: attachment; filename="xxx.jpg"中的filename只支持 ASCII,中文要用filename*=UTF-8''xxx.jpg编码(见下一点) -
Content-Length必须准确,否则某些客户端(如 iOS Safari)会拒绝下载或截断;用filesize($path)获取,别手写
中文文件名在 Chrome/Firefox/Edge 下乱码?用 RFC 5987 编码
直接拼 filename="测试.pdf" 在大多数浏览器里会变成乱码或截断——因为 Content-Disposition 的原始规范不支持 UTF-8 字符。现代做法是用 filename* 参数,按 RFC 5987 编码。
实操建议:别自己写 urlencode,用 PHP 内置的 rawurlencode() + 固定前缀:
立即学习“PHP免费学习笔记(深入)”;
header('Content-Disposition: attachment; filename="download.pdf"; filename*=UTF-8\'\'' . rawurlencode($zh_name));
注意:filename(无星号)是 fallback,必须保留一个 ASCII 文件名;filename* 才是真实生效的 UTF-8 名。Safari 15.4+ 支持,旧版 Safari 只认 filename,所以 fallback 不能空。
- Windows 上 IE/Edge Legacy 只认
filename,且只接受 GBK 编码(已淘汰,不建议兼容) - 别用
iconv()或mb_convert_encoding()转文件名编码——RFC 5987 明确要求 UTF-8 原始字节 +rawurlencode - 如果
$zh_name来自用户输入,务必先过滤路径遍历(如basename($zh_name)),再编码
大文件下载卡死或内存爆掉?必须用 readfile() + ob_flush()
用 file_get_contents() 读取再 echo,等于把整个文件加载进 PHP 内存——100MB 文件就占 100MB 内存,超时、OOM、响应延迟全来了。
正确做法是流式输出:让 Web 服务器(如 Nginx)接管后续传输,PHP 只负责发 header 和分块读取。
- 用
readfile($path)直接输出,它底层调用系统 sendfile()(Linux)或 TransmitFile(Windows),零拷贝、不占 PHP 内存 - 加上
ob_end_clean()清掉之前可能存在的输出缓冲,否则 header 会失效 - Nginx 用户额外加
header('X-Accel-Redirect: /internal/' . basename($path));更高效(需配置 location ~ ^/internal/),但前提是文件不在 webroot 下 - Apache 用户可考虑
mod_xsendfile,原理类似,但启用成本略高
下载链接被缓存、重复触发、或 404?注意路径和权限控制
很多“下载功能”上线后出问题,不是 header 写错,而是文件路径没校验、权限没隔离、或 URL 被 CDN/浏览器缓存。
典型场景:用户传 ?file=../../etc/passwd,或下载链接长期有效导致被盗链。
- 绝对路径必须用
realpath()+ 白名单目录比对,例如:if (strpos(realpath($path), '/var/www/files/') !== 0) die('Access denied'); - 别把真实路径暴露在 URL 里,用 ID 查数据库映射真实路径,或生成带签名的临时 token(如
download.php?id=123&sig=abc) - 加
header('Cache-Control: no-store, no-cache, must-revalidate, max-age=0');防止 CDN 或代理缓存下载响应 - 下载完成后不要留 open_basedir 或 disable_functions 漏洞——
readfile()仍受其限制,确保目标路径在允许范围内
最常被忽略的是:没检查文件是否存在或是否可读就发 header,结果返回空响应,浏览器卡住等数据。每次 readfile() 前加 is_readable($path) 判断,比报错更友好。











