download.php 应置于 web 根目录外或受 .htaccess 保护;大文件用 fopen + fpassthru 并清理输出缓冲;需手动处理 range 请求返回 206 状态;中文文件名按 user-agent 分编码;启用 ignore_user_abort 和 set_time_limit(0) 防进程卡死。

download.php 必须放在 Web 根目录外或受 .htaccess 保护
直接把下载脚本放 public/ 或 htdocs/ 下,等于把门钥匙交出去。哪怕加了权限判断,一旦 PHP 解析失败或配置出错,download.php?file=../config.php 就可能被裸奔执行。
- 最佳位置是
/var/www/private/download.php(Apache DocumentRoot 外),再通过Alias或反向代理暴露入口 - 若必须放 Web 目录内,至少配
.htaccess:禁止直接访问敏感参数,例如RewriteRule ^download\.php$ - [F],只允许特定路径转发 - 检查
open_basedir是否限制了脚本可读取的路径范围,避免绕过白名单读取系统文件
用 fpassthru() 而不是 readfile() 处理 >10MB 文件
readfile() 会把整个文件一次性加载进内存,PHP 进程在传输 100MB 文件时很容易触发 memory_limit 溢出,Nginx 返回 502、Apache 杀进程、浏览器卡死都是常见现象。
- 改用
fopen('path', 'rb')+fpassthru(),内存占用恒定在 ~64KB,不随文件大小增长 - 务必在
fpassthru()前调用ob_end_clean(),否则输出缓冲区里残留的空格或 BOM 会导致 HTTP 头重复或损坏 - 别忘了
fclose($fp)和exit,防止后续代码意外输出干扰二进制流
Range 请求必须手动解析,否则断点续传失效
浏览器点击暂停再继续、wget -c、IDM 多线程拉取,都会发 Range: bytes=1000-2000 请求。PHP 默认不识别这个头,直接返回 200 + 全量内容,客户端就认为“断点失败”,重头开始下。
- 先检查
$_SERVER['HTTP_RANGE']是否存在且匹配bytes=(\d+)-(\d+)? - 必须返回
HTTP/1.1 206 Partial Content状态码,不能只写header('HTTP/1.1 206')—— 它不会覆盖默认的 200 - 要同步设置
Content-Range(如bytes 1000-2000/5000)、Content-Length(分段长度)和Accept-Ranges: bytes - 用
fseek($fp, $start)定位,再fread($fp, $length)读取,别用fpassthru()否则会吐完整文件
中文文件名要按 User-Agent 分编码,不能硬套 rawurlencode
Chrome 和 Edge 支持 filename*=UTF-8''xxx,Firefox 和 Safari 却更认 filename="xxx" + GBK 编码(Windows 平台)。全用 rawurlencode() 会导致 Safari 下显示为一堆 %E4%B8%AD%E6%96%87,IE 则直接截断。
- 提取
$_SERVER['HTTP_USER_AGENT'],用strpos()判断是否含Chrome、Edg、Safari、Firefox - Chrome/Edge:用
rawurlencode()+filename*=UTF-8''格式 - Safari/Firefox/旧 IE:用
iconv('UTF-8', 'GBK', $name)+filename="..." - 永远在
Content-Disposition中同时提供两种格式:filename="xxx"; filename*=UTF-8''xxx
实际部署时最容易被忽略的是 ignore_user_abort(true) 和 set_time_limit(0) 的组合——用户关掉浏览器 tab,PHP 进程却还在 fread 循环里卡着,既占资源又无法释放句柄。这不是 bug,是设计选择:你得自己决定要不要支持中断,而不是依赖默认行为。
php免费学习视频:立即使用
踏上前端学习之旅,开启通往精通之路!从前端基础到项目实战,循序渐进,一步一个脚印,迈向巅峰!











