PHP-防止静态资源被直接访问

mattuy 2018年08月14日 243次浏览

用PHP写后端,想要达到用户登录后才可以访问一些图片和视频资源的效果,因此要阻止用户直接输入资源地址访问资源。

找了一些资料,自己总结了几种方法。

1.根据Referer头——防盗链

浏览器在发起HTTP请求时一般都会一同发送Referer头。Referer头是用户跳转前的页面,也就是通过哪个页面发起的请求。通过禁止非法Referer头的资源请求可以一定程度防止资源被非法访问。一般这个都是在web服务器软件上设置而不是后端处理。由于这个方法并不是很靠谱所以没试过(毕竟请求头是由请求方控制的),不过应付一般用户足够了,特别适合用来防止其他网站挂自己服务器的资源链接以转移服务器负载,不过一般小网站用不到就是了。

2.通过复杂文件名

给资源文件赋以随机的文件名,用数据库记录,然后定期或不定期更新文件名,用户访问页面时后端php动态的查询资源文件名。

这个方法还算可以,缺点是频繁的数据库连接将会增大服务器负载。如果服务器支持可以试试数据库持久连接,不过需要注意持久连接的一些坑,不然可能造成连接锁死之类的问题。

3.隐藏资源——将资源文件放在用户无法访问的目录。

这样做有两种方案可以选择,一是在用户需要访问时将资源文件复制到相应位置(可通过创建硬链接避免时间和空间浪费),二是将所有对资源的访问重定位到一个文件,后端统一验证身份后输出文件内容。

第一种其实意义不大,因为总是要让用户访问的,那只要有授权用户在需要资源文件,你就得把资源放在那里,然后就谁都可以访问了,再然后发现问题回到方法2了——还是得改文件名。

第二种方法是比较靠谱的方法。比如我将资源访问重定向到resource.php这个文件,然后验证身份后根据GET请求参数去找用户请求的文件,然后用readfile函数读取并输出文件就OK。用户的看到的网页源代码将类似这样:

<img src="http://127.0.0.1/resource.php?path=filename"/>

filename可以是真正的资源相对路径,因为用户反正是无法直接访问的,暴露文件路径反而可以省去查数据库的消耗。需要注意的是,如果需要传递的文件路径中包含特殊字符如“/”等需要转义。可在后端统一由某个接口封装,生成安全的资源链接,类似这样:

<?php
function getURL($path) {
    return 'http://127.0.0.1/resource.php?path=' . urlencode($path);
}
?>

<body>
  <img src="<?php echo getURL('picture/dog.jpg')?>">
</body>

然后resource.php中验证用户是否已授权,如果是且资源访问合法,readfile('/resource_path/' . $_GET['path']),结束。

这应该是目前最靠谱的办法,不过它也有缺陷。如果请求的资源是视频这种比较大的文件,浏览器会一直等待资源接收完毕才显示后面的内容,而不是页面加载完再以流媒体的方式加载视频资源,因此这个方法无法用于大文件资源。同时,测试发现,css中的url()资源不支持这样的方式,因此背景图片之类的资源也无法使用这种方法。

4.大杂侩——结合两种方法

非常不幸,我要做的东西正好需要请求大量视频资源,因此采用方法3中的第二类发现浏览器一直转加载视频,后面的评论等板块得等视频下载完成才加载,完全不能忍。于是想了半天,采取了折中的办法:对于小图片、少量图片、文本资源采用方法3第二方案,也就是资源访问重定向到resource.php统一处理;对于大图片、视频资源、大量图片和css中的资源,则通过临时硬链接的方式。

上面3中已经说了readfile()输出的方法,下面说说硬链接的具体做法。

我们知道,php中有个session的概念,它为一个会话生成一个id,并在客户端以cookie的形式保存,在服务器端创建一个唯一的session文件,用于保存与相关会话相关的数据,每次用户请求中包含的cookie便告诉服务器当前会话的相关数据,比如是否已经登录等。

这里正利用了php的session机制。

我在网站目录中创建了一个temp目录,它包含一个空文件index.html以防止用户直接访问目录看到目录下的文件列表(也可在服务器端配置禁止对目录的访问),因此用户可以访问该目录下的文件而无法得知它包含哪些子目录或文件,这是前提。

当用户页面需要请求一个需要授权访问的资源时,如果用户是授权的(通过session机制判断),后端生成相应资源一个非随机的硬链接。它是这样的一个硬链接:

  1. 它的父目录是temp/当前会话session ID/
  2. 它的文件名由它的相对路径通过哈希算法生成,再加上文件后缀

例如,如果我的资源目录(禁止用户访问)是/resource_path,存在资源/resource_path/video/dog.mp4。网站目录是/(对于用户),包含temp子目录。那么我的页面应该这样写:

<video src="<?php echo '/temp/' . session_id() . '/' . md5('video/dog.mp4') . '.mp4'?>"></video>

之前的getURL函数变成这样:

function getURL($path, $flow = false) {
    //非法资源
    if(!file_exists('/resource_path/' . $path)) {
        return '';
    }
    //小文件资源,采用资源重定向方案
    if(!$flow) {
        return '/resource.php?path=' . urlencode($path);
    }
    //获取后缀
    preg_match('/\.[^\.\/]*$/', $path, $sufix);
    $filename = '/temp/'. session_id() . '/' . md5($path) . $sufix[0];
    //判断资源链接是否已创建,WWW_ROOT为网站根目录实际路径
    if(file_exists(WWW_ROOT . $filename)) {
        return $filename;
    }
    //创建temp/sessionID目录
    else if(!file_exists(dirname(WWW_ROOT . $filename))) {
        mkdir(dirname(WWW_ROOT . $filename));
    }
    //创建硬链接。注意Windows不支持PHP的link函数,看你服务器平台
    if(stripos(PHP_OS, 'WIN') === false) {
        link(HEVER_ROOT . $path, HEVER_ROOT . $filename);
    }
    else {
        system('mklink /H "' . HEVER_ROOT . $filename . '" "' . HEVER_ROOT . $path . '"');
    }
    return HEVER_HOME . $filename;
}

当然,md5()的参数也可以另外的算,不一定是相对路径。

这样做的好处是,当用户再次请求相同的资源时,只需确保相应的硬链接存在即可直接不管了,因此需要这个硬链接的文件名是非随机的避免查询数据库。而且这样多了一层session id阻隔,即使非法用户知道了生成资源链接文件名的规则也无法访问到资源,因为资源链接是在当前会话的session id目录下的,除非他能猜到某个会话的session id再模拟发送cookie——还不如让他猜用户名和密码呢。

当然这个方法也不是完美的,由于可能会生成大量的硬链接和session文件并且废弃后不会消失,需要定期执行清理脚本来清除过期的链接和文件。我的解决方案是,当用户登录或登出时触发一个脚本,它会清除超过1天未访问过的session文件和对应的temp目录中的同名名录(其实不是同名,session文件还有sess_前缀)。这个灵感来自于wordpress的伪cron机制。

需要注意的是,Windows系统不支持php的link函数,因此得用windows的shell。还有一点,清理php session文件时,如果php.ini中未配置session.save_path,在php中用session_save_path()可能获取不到php默认的session文件存放目录,因此建议在文件首主动配置session目录,使用session_save_path(string $path)函数。

注:以上方案均未经过严密的测试,请谨慎参考。