各位铁杆粉丝,欢迎回到我们的硬核编程专栏。我是你们的老朋友,那个总是半夜因为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);
?>
代码解析:
fopen(..., 'rb'):用二进制只读模式打开文件。这是处理视频、图片等二进制文件的神器。rb保证了不管系统是什么编码,字节数据都能准确读取。fseek:这是流式传输的灵魂。如果浏览器请求“从第1000字节开始读”,我们就用fseek把文件指针挪到那里,而不是把整个文件从头读到尾。flush():这行代码非常关键。如果不加它,PHP会把所有数据攒在内存里,等攒满了再一次性扔给浏览器。对于视频来说,这会导致前5秒钟缓冲转圈圈。加上flush(),数据一出来就立马走,体验流畅得像开了4G网。Content-Range:告诉浏览器当前发送的数据片段在整个文件中的位置。
现在,盗链者的困难增加了:
他不能直接在浏览器地址栏输入http://www.yourdomain.com/stream_video.php?file=movie.mp4了,因为Referer检查会挂掉。
但是,这还不够。高级的下载工具可以伪造Referer。或者,用户绕过前端,直接用API调用。
第三部分:终极奥义——动态签名
这时候,我们需要一个更硬核的方案:数字签名。这就像是银行的金库,不仅需要门禁卡,还需要密码。
原理:
视频URL中不直接包含文件名,而是包含一个签名和一个时间戳。
-
生成URL:前端或者服务器生成一个URL,比如:
http://www.yourdomain.com/stream_video.php?v=movie.mp4&t=1635365432&sign=abc123xyzv:视频文件名。t:当前时间戳。sign:签名。
-
签名算法:
sign = md5( secret_key + file_name + timestamp )
(这里用了一个简单的算法,实际项目中可能更复杂,比如加盐或使用HMAC)。 -
验证过程:
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
...
为什么它能防盗链?
- 下载麻烦:你无法直接下载一个
.m3u8文件获得视频内容,你只能下载一串.ts文件。 - PHP动态生成:这个
.m3u8文件完全可以由PHP动态生成。我们可以根据刚才讲的Referer检查或签名,只允许合法的播放器请求这个列表文件。 - 灵活性:你可以控制每个切片的时长,甚至实时推流。
用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等级、积分兑换播放权限)。
第五部分:别让服务器罢工——性能与监控
写完代码,只是万里长征走完了第一步。你还得担心两件事:带宽成本和服务器崩溃。
-
带宽监控:
写一个简单的脚本,记录下每次视频请求的大小。// 在 stream_video.php 的最后 $log_file = 'bandwidth.log'; $size = filesize($file_path); // 实际上你应该统计每次 fread 发送的大小 // 记录到日志...如果发现某天日志激增,说明有人大规模盗链了。这时候,你的Referer检查和签名机制就该上线了。
-
缓存策略:
告诉浏览器和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或浏览器缓存拿。
-
白名单机制:
对于VIP用户,或者内部培训视频,不要搞那么严。可以写个简单的逻辑:$user_ip = $_SERVER['REMOTE_ADDR']; $whitelist = ['192.168.1.100', '10.0.0.5']; if (in_array($user_ip, $whitelist)) { // 放行,不检查签名,直接读文件 } else { // 检查签名 }
总结
好了,今天的硬核讲座就到这里。
我们要实现视频在线播放并防盗链,核心思路就两点:
- 不让浏览器直连文件:用PHP做中间人,用
fopen和fread做流式传输。 - 不给链接通行的权利:用Referer卡住初级党,用签名和时间戳卡住高级党。
别再说你的带宽是“风力发电机”了。装上这套PHP流式播放器,配合动态签名,你的服务器就能安安稳稳地度过一个又一个深夜。
如果你在实现过程中遇到CPU飙升,别慌,大概率是你选的chunk_size太小了,或者……有人真的在下载你的视频。
祝你代码无Bug,带宽不爆表!下期见!