
本文介绍通过分块读取替代 readfile() 来防范下载接口被恶意脚本高频调用导致的服务器资源耗尽问题,结合连接中断检测与基础限流策略,提升下载服务的安全性与健壮性。
本文介绍通过分块读取替代 `readfile()` 来防范下载接口被恶意脚本高频调用导致的服务器资源耗尽问题,结合连接中断检测与基础限流策略,提升下载服务的安全性与健壮性。
在基于 PHP 的文件下载服务中,直接使用 readfile() 处理大文件(如 MP4 视频)虽简洁高效,但存在显著安全隐患:当客户端(尤其是自动化脚本)发起请求后立即中断连接,PHP 默认仍会持续执行 readfile(),完整读取整个文件到内存或输出缓冲区——这不仅浪费 I/O 与 CPU 资源,还可能被用于压测或带宽耗尽攻击。
根本原因在于:readfile() 是阻塞式底层 I/O 操作,执行期间 PHP 无法及时响应客户端断连信号(connection_aborted()),即使未显式调用 ignore_user_abort(true),其内部实现也难以在读取过程中周期性检查连接状态。
✅ 推荐解决方案:分块流式输出 + 连接状态实时校验
以下是一个生产就绪的改进版下载脚本,具备三项关键防护能力:
立即学习“PHP免费学习笔记(深入)”;
- ✅ 实时检测客户端是否已断开(connection_status() === CONNECTION_ABORTED 或 connection_aborted())
- ✅ 分块读取(默认 8KB),避免单次大内存占用,同时为中断检测提供检查点
- ✅ 内置基础请求频率限制(可选,建议配合 Redis 或文件锁增强)
<?php
// 启用输出缓冲控制(可选,便于调试)
ob_start();
// 验证并过滤输入参数(务必执行!)
if (!isset($_GET['MP4Address']) || !is_string($_GET['MP4Address'])) {
http_response_code(400);
die('Invalid request');
}
$MP4Address = $_GET['MP4Address'];
// 白名单路径校验(防止目录遍历)
$allowedBaseDir = '/var/www/videos/';
if (strpos(realpath($MP4Address), $allowedBaseDir) !== 0 || !is_file($MP4Address) || !is_readable($MP4Address)) {
http_response_code(403);
die('Access denied');
}
// 设置响应头
$filename = basename($MP4Address);
header('Content-Type: video/mp4'); // 更精准的 MIME 类型优于 application/octet-stream
header('Content-Disposition: attachment; filename="' . urlencode($filename) . '"');
header('Content-Transfer-Encoding: binary');
header('Accept-Ranges: bytes');
// 关键:禁用输出缓冲,确保数据即时发送
if (ob_get_level()) ob_end_flush();
@ini_set('output_buffering', 'off');
@ini_set('zlib.output_compression', 'Off');
@ini_set('implicit_flush', 'On');
ob_implicit_flush(1);
// 分块读取函数(带中断检测)
function streamFile($filepath, $chunkSize = 8192) {
$fh = fopen($filepath, 'rb');
if (!$fh) {
throw new RuntimeException('Cannot open file for reading: ' . $filepath);
}
while (!feof($fh)) {
// 检查客户端是否已断开连接
if (connection_status() !== CONNECTION_NORMAL || connection_aborted()) {
fclose($fh);
error_log("Download aborted by client: $filepath");
return false;
}
$buffer = fread($fh, $chunkSize);
if ($buffer === false) {
break;
}
echo $buffer;
// 强制刷新输出缓冲(确保数据发出,触发连接状态更新)
if (function_exists('flush')) flush();
usleep(1000); // 微小延迟,降低 CPU 占用(可选)
}
fclose($fh);
return true;
}
// 执行下载
if (!streamFile($MP4Address)) {
http_response_code(499); // Client Closed Request (Nginx standard)
die();
}
?>⚠️ 重要注意事项:
- 永远不要跳过输入校验:$_GET['MP4Address'] 必须经过 realpath() + 白名单路径前缀校验,否则存在严重目录遍历风险;
- MIME 类型应准确:对 .mp4 使用 video/mp4 而非泛用 application/octet-stream,有助于浏览器/CDN 正确处理;
- flush() 不是万能的:某些 Web 服务器(如 Nginx)或代理层可能缓存响应,需同步配置 proxy_buffering off; 等参数;
-
进阶防护建议:
- 在入口层添加 IP 限流(如 Nginx limit_req);
- 对高频请求 IP 记录日志并临时封禁(可用 iptables 或 Fail2ban);
- 敏感资源改用一次性签名 URL(如 ?token=sha256(...)&exp=171...),过期即失效;
- 将大文件托管至 CDN 或对象存储(如 S3 + Pre-Signed URL),完全卸载 PHP 服务器压力。
通过将粗粒度的 readfile() 替换为可控、可中断的分块流式输出,并辅以严谨的输入验证与基础设施级防护,即可在保持功能完整性的同时,显著降低下载接口被滥用的风险。



















