PHP如何实现视频在线播放并防止视频资源被盗链下载

各位铁杆粉丝,欢迎回到我们的硬核编程专栏。我是你们的老朋友,那个总是半夜因为CPU风扇狂转而惊醒的资深PHP工程师。

今天我们聊个有意思的话题:视频在线播放与防盗链

想象一下,你辛辛苦苦拍了部微电影,剪辑、调色、渲染,终于上传到了服务器。你满怀期待地发给朋友们:“来,看看我的处女作!”朋友们兴高采烈地点开链接,画面开始播放,美酒加咖啡,人生很完美。

但就在你准备收工睡觉的时候,你的服务器报警短信像催命符一样响了:“带宽占用率100%”,“CPU过热”,“磁盘写入错误”。你惊坐而起,打开控制台一看,好家伙,你的视频流量一夜之间跑了几个T。

你满腹委屈地问:“我就发了两个链接,怎么消耗了那么多流量?”

答案很扎心:你的朋友们可能把你的视频当成了百度网盘的免费会员,把你的服务器当成了公共网盘。 他们不仅看了,还把链接分享到了群里,结果你的服务器成了全互联网的免费硬盘。

这就像是你请客吃饭,大家不仅吃了,还打包了整个餐厅的菜,连锅都端走了。

今天,我们就来聊聊如何用PHP这把手术刀,把“打包党”拦在门外,同时让视频播放丝般顺滑。我们将分三步走:基础的Referer拦截、进阶的PHP流式传输、以及终极的动态签名防盗链。

第一部分:HTML5的陷阱与Referer的假象

首先,我们得搞清楚视频是怎么被播放的。现在的浏览器,基本都支持HTML5的<video>标签。

最简单的代码长这样:

<!DOCTYPE html>
<html>
<body>

<video width="640" height="480" controls>
  <source src="movie.mp4" type="video/mp4">
  您的浏览器不支持 Video 标签。
</video>

</body>
</html>

这就叫“裸奔”。只要有人访问这个网页,浏览器就会向服务器发起一个请求:GET /movie.mp4。服务器一看:“好的,给文件。”于是,流量就开始了。

这时候,第一个防君子不防小人的大招来了——HTTP Referer(来源)检查

大家有没有遇到过这种情况:你直接在浏览器地址栏输入图片地址,图片显示不出来,但嵌入在网页里却能显示?这就是因为HTTP请求头里的Referer字段在作祟。

我们可以在PHP里写个简单的中间件:

<?php
// video.php
$file = 'movie.mp4'; // 视频文件路径

// 获取Referer头
$referer = isset($_SERVER['HTTP_REFERER']) ? $_SERVER['HTTP_REFERER'] : '';

// 允许的域名白名单
$allow_domains = ['http://www.yourdomain.com', 'http://yourapp.com'];

// 检查逻辑
$is_valid = false;
foreach ($allow_domains as $domain) {
    if (strpos($referer, $domain) !== false) {
        $is_valid = true;
        break;
    }
}

if (!$is_valid) {
    header('HTTP/1.1 403 Forbidden');
    echo '禁止访问:你不在我的名单上。';
    exit;
}

// 如果合法,继续播放逻辑
// 这里就是我们要讲的PHP流式传输,先别急
readfile($file);
?>

这段代码的原理:
当浏览器播放视频时,如果是在你的网页(www.yourdomain.com)内,请求头会带上Referer:http://www.yourdomain.com。如果用户直接复制这个video.php的链接去浏览器打开,或者用下载工具(迅雷、IDM)下载,Referer通常是空的,或者来自搜索引擎,直接就会被这个逻辑挡在门外。

但是,这玩意儿靠不住。
为什么?因为聪明的黑客或用户可以安装浏览器插件,伪造Referer头。这就好比你在门口装了个保安,那个保安戴着面具,谁都能进去。

所以,我们得升级。我们需要一个“动态通行证”。

第二部分:PHP流式传输——这才是正道

要防止下载,最核心的思想是:不要让浏览器直接请求视频文件路径

想象一下,服务器上的视频文件是/var/www/html/videos/abc.mp4。如果用户直接访问这个路径,他就可以右键“另存为”,连PHP代码都不用跑。

所以,我们要把视频文件藏在PHP脚本后面。所有对视频的请求,都必须经过PHP处理。

这时候,我们就得祭出PHP流式传输了。为什么要流式?因为视频文件动不动就几百兆,如果用readfile()一次性读出来,内存会爆炸,而且用户还得傻傻地等文件下载完毕才能开始看前5秒钟。

流式传输就像是用水管接水,你不用把整个游泳池的水都抽干才能看到水龙头里流出的水,它是边读边发的。

下面,我给你们写一个“工业级”的PHP视频流播放器代码。请拿好你的小本本,这可是干货。

<?php
// stream_video.php
// 这个脚本将处理视频请求,并负责“看门”

// 1. 基础配置
$video_dir = __DIR__ . '/videos/'; // 视频存放目录
$video_file = $_GET['file'] ?? ''; // 获取视频文件名
$file_path = $video_dir . $video_file;

// 2. 安全检查:防止路径遍历攻击(防止输入 ../../../../etc/passwd)
if (strpos($file_path, $video_dir) !== 0) {
    die("非法访问路径!");
}

// 3. 检查文件是否存在
if (!file_exists($file_path) || !is_file($file_path)) {
    http_response_code(404);
    echo "视频不存在,或者被删了。";
    exit;
}

// 4. 防盗链核心:检查来源
// 注意:如果前端是在 iframe 中嵌入,Referer 可能是父域名
$referer = $_SERVER['HTTP_REFERER'] ?? '';
$allowed_domain = 'http://www.yourdomain.com'; // 你的主站

if (strpos($referer, $allowed_domain) === false) {
    // 如果没有 Referer,或者 Referer 不合法,这里可以返回一个 403
    // 但为了更好的用户体验,我们可以返回一个“防盗链”提示图片或视频
    // 这里简单处理,直接报错
    header("HTTP/1.1 403 Forbidden");
    echo "<h1>403 - 访问被拒绝</h1><p>请从合法网站访问本视频资源。</p>";
    exit;
}

// 5. 流式传输的关键:处理 HTTP Range 请求
// 浏览器播放器不是一下子把所有数据发过去,它是先发前几秒,缓冲完再发下一部分
// Range: bytes=0-1023 表示请求前 1024 个字节
$range = $_SERVER['HTTP_RANGE'] ?? null;

// 打开文件指针,二进制模式,读指针移到开头
$fp = fopen($file_path, 'rb');

// 如果有 Range 请求
if ($range) {
    // 解析 Range,例如 "bytes=0-1023" -> start=0, end=1023
    $range = str_replace('bytes=', '', $range);
    $range = explode('-', $range);
    $start = intval($range[0]);
    $end = isset($range[1]) ? intval($range[1]) : $file_size - 1;

    // 设置文件指针位置
    fseek($fp, $start);

    // 设置响应头
    header('HTTP/1.1 206 Partial Content');
    header("Content-Range: bytes $start-$end/$file_size");
} else {
    // 如果没有 Range 请求(比如用下载工具直接抓取),则从头开始
    $start = 0;
    $end = $file_size - 1;
    header('Content-Type: video/mp4');
    header('Content-Length: ' . $file_size);
}

header('Accept-Ranges: bytes');

// 6. 核心循环:边读边发
$chunk_size = 8 * 1024 * 1024; // 每次读 8MB,减少IO压力
$buffer = '';
$bytes_read = 0;

while (!feof($fp) && ($bytes_read < $end)) {
    $buffer = fread($fp, $chunk_size);
    echo $buffer;
    // 刷新缓冲区,告诉浏览器“我发了一部分,你赶紧存起来”
    flush(); 
    $bytes_read += strlen($buffer);
}

fclose($fp);
?>

代码解析:

  1. fopen(..., 'rb'):用二进制只读模式打开文件。这是处理视频、图片等二进制文件的神器。rb 保证了不管系统是什么编码,字节数据都能准确读取。
  2. fseek:这是流式传输的灵魂。如果浏览器请求“从第1000字节开始读”,我们就用fseek把文件指针挪到那里,而不是把整个文件从头读到尾。
  3. flush():这行代码非常关键。如果不加它,PHP会把所有数据攒在内存里,等攒满了再一次性扔给浏览器。对于视频来说,这会导致前5秒钟缓冲转圈圈。加上flush(),数据一出来就立马走,体验流畅得像开了4G网。
  4. Content-Range:告诉浏览器当前发送的数据片段在整个文件中的位置。

现在,盗链者的困难增加了:
他不能直接在浏览器地址栏输入http://www.yourdomain.com/stream_video.php?file=movie.mp4了,因为Referer检查会挂掉。

但是,这还不够。高级的下载工具可以伪造Referer。或者,用户绕过前端,直接用API调用。

第三部分:终极奥义——动态签名

这时候,我们需要一个更硬核的方案:数字签名。这就像是银行的金库,不仅需要门禁卡,还需要密码。

原理:
视频URL中不直接包含文件名,而是包含一个签名和一个时间戳。

  1. 生成URL:前端或者服务器生成一个URL,比如:
    http://www.yourdomain.com/stream_video.php?v=movie.mp4&t=1635365432&sign=abc123xyz

    • v:视频文件名。
    • t:当前时间戳。
    • sign:签名。
  2. 签名算法
    sign = md5( secret_key + file_name + timestamp )
    (这里用了一个简单的算法,实际项目中可能更复杂,比如加盐或使用HMAC)。

  3. 验证过程
    PHP脚本收到请求后,解密这三个参数:

    • 检查时间戳:if (time() - $timestamp > 3600) { die("链接过期,请刷新"); }(防止链接被保存下来无限期使用)。
    • 检查签名:if (md5($secret . $file . $t) !== $sign) { die("签名错误,黑客!"); }
    • 通过验证,则播放。

实战代码:

首先是生成签名的辅助函数(通常放在配置文件里):

<?php
/**
 * 生成带签名的视频播放URL
 * @param string $file 视频文件名
 * @return string 完整URL
 */
function build_secure_url($file) {
    $secret_key = 'MySuperSecretKey123!'; // 服务端密钥,绝对不能泄露给前端
    $timestamp = time();
    $sign = md5($secret_key . $file . $timestamp);

    // 假设当前域名
    $domain = 'http://www.yourdomain.com';
    $path = '/stream_video.php';

    // 拼接参数
    $url = $domain . $path . "?v={$file}&t={$timestamp}&sign={$sign}";

    return $url;
}

// 测试:生成一个链接
// echo build_secure_url('movie.mp4');
?>

然后是修改我们的视频流脚本,加入验证逻辑:

<?php
// stream_video.php (带签名验证版)

$secret_key = 'MySuperSecretKey123!';
$file = $_GET['v'] ?? '';
$t = $_GET['t'] ?? '';
$sign = $_GET['sign'] ?? '';

// 1. 基础校验:参数不能少
if (!$file || !$t || !$sign) {
    die("缺少必要参数");
}

// 2. 时间校验:防止链接被保存
// 比如链接有效期只有 1 小时
if (abs(time() - $t) > 3600) {
    die("链接已过期,请刷新页面。");
}

// 3. 签名校验
// 这里的 secret_key 必须和生成链接时的一致
$expected_sign = md5($secret_key . $file . $t);

if ($sign !== $expected_sign) {
    // 记录日志:记录一下是谁在尝试攻击,这能帮你抓黑客
    // error_log("Sign check failed for file: $file, IP: " . $_SERVER['REMOTE_ADDR']);
    die("签名错误,非法请求!");
}

// 如果验证通过,执行之前的流式传输逻辑...
// (这里为了节省篇幅,省略了重复的流式传输代码,请把上面的 fread/flush 逻辑搬过来)
// 注意:引入流式传输逻辑时,记得带上 $file_path 的获取逻辑

// ... 假设获取到了 $file_path
$file_path = __DIR__ . '/videos/' . $file;
// ... 继续上面的流式传输代码
?>

这种方案有多牛?
即使黑客获取到了这个URL,他也没法把链接给别人,因为时间戳一过就失效了。而且,没有密钥,他根本算不出正确的签名。

第四部分:进阶防御——HLS 与 DASH

虽然PHP代理和签名已经能解决大部分问题,但总有一些不知疲倦的黑客。而且,直接传输MP4大文件对服务器CPU和内存压力巨大。

这时候,我们需要搬出视频界的“变形金刚”——HLS (HTTP Live Streaming)DASH

HLS 是什么?
HLS不是一种视频格式,而是一种分发协议。它把一个完整的MP4视频切片成几十个小段(比如每段10秒),然后生成一个.m3u8的播放列表文件。

你打开一个HLS链接,比如 http://site.com/playlist.m3u8,你会发现里面只有一堆小文件的引用,比如:

#EXTM3U
#EXT-X-VERSION:3
#EXTINF:10.0,
segment_0.ts
#EXTINF:10.0,
segment_1.ts
...

为什么它能防盗链?

  1. 下载麻烦:你无法直接下载一个 .m3u8 文件获得视频内容,你只能下载一串 .ts 文件。
  2. PHP动态生成:这个 .m3u8 文件完全可以由PHP动态生成。我们可以根据刚才讲的Referer检查签名,只允许合法的播放器请求这个列表文件。
  3. 灵活性:你可以控制每个切片的时长,甚至实时推流。

用PHP实现HLS切片生成器:

假设你的视频文件夹里全是MP4文件,我们写个脚本扫描文件夹,生成M3U8列表。

<?php
// generate_playlist.php
$video_dir = __DIR__ . '/videos/';
$files = scandir($video_dir);

// 过滤掉 . 和 ..
$videos = array_filter($files, function($f) {
    return $f != '.' && $f != '..' && pathinfo($f, PATHINFO_EXTENSION) == 'mp4';
});

header('Content-Type: application/vnd.apple.mpegurl');
header('Cache-Control: no-cache, no-store, must-revalidate'); // 播放器通常不缓存列表

echo "#EXTM3Un";
echo "#EXT-X-VERSION:3n";
echo "#EXT-X-STREAM-INF:BANDWIDTH=800000,RESOLUTION=640x360n"; // 360P 流
echo "360p.m3u8n"; // 这是个子列表,逻辑类似,也可以直接指向ts切片

// 这里我们演示简单模式:直接列出视频文件作为片段
foreach ($videos as $video) {
    // 为了安全,我们在这里也加一个防盗链检查
    // 但通常列表是放在前端HTML里的,由前端去请求
    // 我们这里只负责把文件列表吐出来

    // 假设我们给每个文件加了时间戳参数
    $timestamp = time();
    $sign = md5('secret' . $video . $timestamp);

    echo "#EXTINF:10.0,n";
    echo "stream_video.php?v={$video}&t={$timestamp}&sign={$sign}n";
}
?>

注意:真正的HLS服务(比如NGINX-RTMP模块)在切片效率上完爆PHP。如果你要做生产级的流媒体服务,强烈建议使用 NGINX + FFmpeg 来做切片和推流。PHP在这里更适合做Web端的业务逻辑控制(比如判断用户VIP等级、积分兑换播放权限)。

第五部分:别让服务器罢工——性能与监控

写完代码,只是万里长征走完了第一步。你还得担心两件事:带宽成本服务器崩溃

  1. 带宽监控
    写一个简单的脚本,记录下每次视频请求的大小。

    // 在 stream_video.php 的最后
    $log_file = 'bandwidth.log';
    $size = filesize($file_path); // 实际上你应该统计每次 fread 发送的大小
    // 记录到日志...

    如果发现某天日志激增,说明有人大规模盗链了。这时候,你的Referer检查和签名机制就该上线了。

  2. 缓存策略
    告诉浏览器和CDN,视频是可以缓存的。

    // 在 stream_video.php 头部添加
    header('Cache-Control: public, max-age=3600'); // 缓存1小时
    header('Expires: ' . gmdate('D, d M Y H:i:s', time() + 3600) . ' GMT');

    这样,用户第二次看视频,就不需要再请求你的PHP服务器了,直接从CDN或浏览器缓存拿。

  3. 白名单机制
    对于VIP用户,或者内部培训视频,不要搞那么严。可以写个简单的逻辑:

    $user_ip = $_SERVER['REMOTE_ADDR'];
    $whitelist = ['192.168.1.100', '10.0.0.5'];
    
    if (in_array($user_ip, $whitelist)) {
        // 放行,不检查签名,直接读文件
    } else {
        // 检查签名
    }

总结

好了,今天的硬核讲座就到这里。

我们要实现视频在线播放并防盗链,核心思路就两点:

  1. 不让浏览器直连文件:用PHP做中间人,用fopenfread做流式传输。
  2. 不给链接通行的权利:用Referer卡住初级党,用签名和时间戳卡住高级党。

别再说你的带宽是“风力发电机”了。装上这套PHP流式播放器,配合动态签名,你的服务器就能安安稳稳地度过一个又一个深夜。

如果你在实现过程中遇到CPU飙升,别慌,大概率是你选的chunk_size太小了,或者……有人真的在下载你的视频。

祝你代码无Bug,带宽不爆表!下期见!

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注