PHP 大文件下载防盗链

wen PHP项目 2

PHP大文件下载防盗链的终极指南:从Referer校验到加密签名与流式传输


目录导读(Table of Contents)

  1. 为什么大文件下载需要防盗链? – 带宽盗用与Hotlink的危害
  2. 基础防护:HTTP Referer校验及其局限性 – 别把安全寄托在Header上
  3. 进阶策略:临时签名URL(Tokenized Link) – 时间戳+HMAC的黄金组合
  4. 终极方案:PHP流式传输+加密Cookie – 大文件不占内存,且无法被直接解析
  5. 性能与安全平衡:X-Sendfile / X-Accel-Redirect – 让Nginx/Apache替你干活
  6. 常见问题与实战问答(FAQ) – 针对你踩过的坑

为什么大文件下载需要防盗链?

当你的服务器托管着高清视频、软件安装包或压缩资源时,一旦被其他网站(如论坛或盗版站)直接引用下载链接(<a href="https://yourdomain.com/file.zip">),每次用户点击都会消耗你的带宽、CPU和I/O,这种“Hotlink”行为轻则拖垮服务器,重则产生高额CDN账单。

PHP 大文件下载防盗链

大文件尤为致命:一次1GB的下载请求如果没被拦截,可能瞬间吃掉你月流量的10%,防盗链不仅是安全需求,更是成本控制手段

基础防护:HTTP Referer校验及其局限性

实现逻辑:检查$_SERVER['HTTP_REFERER'],如果不是来自你网站域名的请求,直接header('HTTP/1.1 403 Forbidden')

$allowed_domains = ['yourdomain.com', 'www.yourdomain.com'];
$referer = $_SERVER['HTTP_REFERER'] ?? '';
if ($referer) {
    $host = parse_url($referer, PHP_URL_HOST);
    if (!in_array($host, $allowed_domains)) {
        http_response_code(403);
        exit('Forbidden');
    }
} else {
    // 没有Referer(如浏览器直接输链接、迅雷下载),这里策略要慎重
    http_response_code(403);
    exit('Direct link not allowed');
}

致命缺陷

  • Referer值可伪造(用Curl或扩展程序改Header)。
  • 很多下载工具(如IDM、迅雷)默认不发送Referer,导致合法用户被误杀。

Referer只能当“礼貌性阻拦”,绝不能作为唯一防线。

进阶策略:临时签名URL(Tokenized Link)

核心思想:下载链接必须携带一个时效性+权限的签名,链接过期或篡改则失效。

生成签名(在受保护的下载页/接口中):

$secret = 'your-32-char-random-secret'; // 务必保密
$file_id = 1024;
$expire = time() + 3600; // 1小时后过期
$token = md5($secret . $file_id . $expire);
$download_link = "/download.php?file={$file_id}&exp={$expire}&token={$token}";
// 输出到页面:<a href="...">下载</a>

验证签名(download.php):

$file_id = (int)$_GET['file'];
$expire = (int)$_GET['exp'];
$token = $_GET['token'];
if ($expire < time() || $token !== md5($secret . $file_id . $expire)) {
    http_response_code(403);
    exit('Link expired or invalid');
}
// 通过验证,进入文件处理逻辑
$file_path = '/data/files/' . $file_id . '.zip';
// … 下面用流式下载(见第4节)

优点:即使别人抓取到链接,只要过期就无法使用,且无法伪造新链接。

终极方案:PHP流式传输+加密Cookie(针对大文件)

痛点:上述签名URL如果被分享,有效期内的链接依然可被任何人下载,对于大文件,我们需要“每个用户独立鉴权+下载过程不落地”

方案:用户必须先登录系统,下载时后端生成一个随机密钥,加密Cookie中保存用户ID+IP+过期时间,下载脚本里验证Cookie,并用readfile()或流式循环输出。

流式传输代码(不占内存):

// 已验证用户合法性 & Cookie签名有效
$file_path = '/data/downloads/huge_video.mp4';
if (!file_exists($file_path)) {
    http_response_code(404);
    exit;
}
header("Content-Type: application/octet-stream");
header("Content-Length: " . filesize($file_path));
header("Content-Disposition: attachment; filename=\"movie.mp4\"");
// 关键:打开文件并进行指针控制
$fp = fopen($file_path, 'rb');
while (!feof($fp)) {
    echo fread($fp, 8192); // 每次读8KB,输出给客户端
    ob_flush();
    flush();
}
fclose($fp);

加强防盗链:在输出前检查$_COOKIE['secure_download'],该Cookie由登录态签发,并且设置HttpOnlySecureSameSite,攻击者即便拿到URL,没有Cookie依然403。

性能与安全平衡:X-Sendfile / X-Accel-Redirect

PHP循环输出对于1GB文件来说效率太低。最佳实践是让PHP只做验证,然后移交Web服务器发送文件。

Nginx方案(推荐):

// 验证通过后,代替fread循环:
header('X-Accel-Redirect: /protected_files/movie.mp4');
// 关键:需要在nginx.conf里将/protected_files/映射到内部location,且不允许直接访问

Apache方案

header('X-Sendfile: /data/local/files/movie.mp4');
// 需安装mod_xsendfile,且注意物理路径保护

优点:PHP进程瞬间释放,服务器零内存消耗,传输速度极快,且文件物理路径对用户完全不可见。


常见问题与实战问答(FAQ)

Q1:用户下载中断后重试,是否每次都要重新登录? :不需要,把Cookie过期时间设为30分钟或更长,只要Cookie有效,就可以重新发起下载,签名URL也可以设定更长有效期(如24小时),但不要用"永久"签名。

Q2:如何防止多线程下载器(如IDM)绕过签名? :IDM会带Referer,且支持Range请求,你的下载脚本必须忽略Range头(直接看用户是否可以获取整个文件),但这样会导致下载工具无法续传,若想支持续传又防盗链,可以联合签名URL+动态Cookie,并且开启Nginx的limit_rate控制速度。

Q3:我用了加密Cookie,但用户还是可以复制Cookie给朋友用,怎么办? :绑定IP(但动态IP会误杀),更强的是绑定User-Agent+IP组合,但是不要过度安全,导致合法用户(如公司多设备、IPv6切换)被频繁拒绝,折中方案:Cookie有效期缩短至5分钟,且每次下载都重置。

Q4:整个文件输出时,如果用户点了浏览器停止,PHP进程会一直运行吗? :会,所以你必须开启ignore_user_abort(false)(默认是false),并且每输出一段检查connection_aborted(),发现断开立即关闭文件指针并释放资源。

Q5:大文件下载时,服务器内存总是爆满,是fread的问题吗? :不是,如果你用了file_get_contents()读全文件到内存,那肯定爆,使用fread+flush()是没问题的,但更好的方案是挪到Nginx X-Accel,彻底解决内存问题。


三层防线怎么搭?

  1. 第一层:Referer过滤(拦截小白爬虫)。
  2. 第二层:签名URL(防链接盗用)。
  3. 第三层:登录态Cookie+IP绑定+流式传输(守住核心资源)。

无论你用哪种,请始终记得:大文件防盗链的本质不是"加密文件",而是"控制链接的合法使用人",架构上,优先考虑Nginx的反向代理加分发,PHP只做鉴权逻辑,这样既有安全,又有飞一般的下载体验。


(文章完)

抱歉,评论功能暂时关闭!