Nginx实现防盗链
Table of Contents
什么是盗链及防盗链的方法
什么是盗链
盗链就是别的网站直接使用(即盗用)你网站中的文件的链接(通常盗用是图片链接比较多,也有可能是zip/rar压缩包,mp3、mp4等等)。
盗链导致的问题:如果有很多网站用了你网站的图片链接,或者虽然只有一个网站盗用你网站的链接,但是它的浏览量非常大,图片每被浏览一次,就要消耗你一次你服务器的流量和cpu/内存等资源,或者消耗你的付费CDN的流量,而占用你服务器资源会导致你服务器压力增大,另外流量都是要花钱的,显然我们不希望发生这样的事情!
防链接的方法
- 1、通过识别Referer(很容易被绕过);
- 2、通过识别User-Agent(很容易被绕过);
- 3、通过登录验证(不登录就可以浏览的网站并不适合);
- 4、通过在url中设置验证参数(如过期时间,md5值),比较推荐;
通过识别Referer防盗链
Referer防盗链原理
前面说了,盗链是别的网站直接盗用了你网站的图片,那么如果我们有方法能知道是别的网站在访问你网站的图片,然后对这个请求进行禁止,就可以达到防盗链的效果了。
那有什么方法可以知道到底是不是自己的网站在访问自己网站的图片呢?答案是使用HTTP请求头中的Referer属性。
HTTP的Referer属性用于标记请求是从“哪儿”过来的(这个“哪儿”是指一个具体的链接)。
Referer这个单词有两个er,有些朋友可能经常记不住,其实它是refer+er,refer是参考/引用/将……递交给……的意思,而添加了er一般都会加个“者”,比如work是工作,worker就是工作者(即工人);teach是教学,teacher就是教学者(即老师),与这个类似,refer是参考,加个er变成referer就是参考者/引用者的意思,用于表示是从哪儿引用过来的。
HTTP协议Referer规则:
- 1、如果你直接在浏览器输入并回车访问的当前页面,那么Referer属性是不存在的,再次刷新页面,也是不存在的;
- 2、如果你在别的网页通过“点击链接”访问的当前页面,那么Referer属性就是“别的网页”的链接,注意,“别的网页”也可以是“当前网页”;
- 3、如果链接被防火墙标记过,则Referer值为blocked。
把以下代码保存到test.php
中,并在你本地localhost访问它(这个需要你自己配置好nginx服务器)
<?php
// 直接在浏览器中输入地址,请求该页面,是没有
if(isset($_SERVER['HTTP_REFERER'])){
var_dump($_SERVER['HTTP_REFERER']);
}else{
echo '没有HTTP_REFERER<br>';
}
?>
<a href="./test.php">走你</a>
如下所示,如果直接在浏览器输入地址访问test.php,是没有HTTP_REFERER的
点“走你”后还是跳转到test.php,但此时就有Referer了
所以我们如果要允许一张图片在本站显示,需要允许三种情况:Referer为空、Referer为blocked、Referer为本站域名(可多个),除此之外的其它情况,都给它返回一个403 Forbidden错误,或者其它你想返回的内容也行。
Nginx设置Referer防盗链
主要是通过location来识别到对应的后缀后进行判断
location ~ .*\.(wma|wmv|asf|mp3|mmf|zip|rar|jpg|gif|png|swf|flv|mp4)$ {
valid_referers none blocked www.xiebruce.top.local www.xiebruce_local.top;
if ($invalid_referer) {
# 这里也可以用rewrite /path/to/img.jpg 来显示一张提示防盗链的图片
return 403;
}
}
valid_referers
,指定有效的referer值,一般有三种:none(直接输入网址访问)、blocked(被防火墙标记)、允许访问的域名(可多个),这些值之间用空格隔开即可;invalid_referer
,当请求来源的referer值不符合valid_referers定义的值时,$invalid_referer
为true,然后就会进入if里面,你可以在里面return 403或者rewrite到固定的图片中。
绕过Referer防盗链
由于Referer都是允许为空的(否则将会导致直接在浏览器输入链接时无法访问的情况),所以我们只要把我们的请求去除Referer值即可。
去除的方法有以下几种:
- 通过http响应头中的Referrer-Policy字段;
- 通过meta标签,name为referrer;
- 通过a、area、img、iframe、link元素的referrerpolicy属性;
- 通过a、area、link元素的rel=noreferrer属性。
以img为例,直接在img标签中用referrerPolicy="no-referrer"
即可把HTTP请求中的Referer值
<img src="http://b.test.com/fireworks.jpg" height="500px" referrerPolicy="no-referrer"/>
当然也可以用meta标签,这样整个页面所有链接都会没有Referer值
<meta name="referrer" content="no-referrer" />
<img src="http://b.test.com/fireworks.jpg" height="500px"/>
特别注意:在html中或js中设置这个Referer,需要写成Referrer,即多了一个r,这个其实是英语单词中的一个规则,一些r结尾的单词,如果r前是单音节且重音在后面,则加er或ed时,需要双写r,比如occur与occurred,prefer与preferred,transfer与transferred,这些单词都是重音在后面,所以加er或ed时需要双写r,而appear与appeared就不需要双写r,因为appear中r前面是双音节。
根据以上原理,refer也是重音在后,r前也是单音节,所以它加er或ed时需要双写r,变成referrer、referred,但是为什么HTTP协议的属性里没有双写呢?我猜是当时HTTP协议的作者拼写错误,有人可能会说,人家作者是英国人,本来就说英文,怎么会拼错?我想说,你自己就没有写错汉字的时候?
Nginx通过secure_link方式防盗链
这是ngx_http_secure_link_module官方文档。
secure_link防盗链原理
secure_link指令需要编译“ngx_http_secure_link_module”模块,该模块nginx自带但默认是不启用的,你需要在编译nginx时,使用以下指令指定启用该模块
--with-http_secure_link_module
该模块有三个指令,分别为:secure_link、secure_link_md5、secure_link_secret,使用方法有两种:
- 1、secure_link + secure_link_secret(v0.7.18以后);
- 2、secure_link + secure_link_md5(v0.8.5以后,其实就是用来替代第一种的)。
由于方案2的效果更好,方案1我就不介绍了,直接说方法2吧。
secure_link + secure_link_md5基本原理:
- 1、把一些参数以及过期时间按你想要的顺序拼成一个字符串,然后对它做md5计算,然后把这个md5值以及过期时间以参数的方式附到链接后面;
- 2、在nginx中我们配置同样的参数串顺序,nginx会按我们给出的参数串计算出它的md5值,然后把它计算得到的md5值与链接中传过来的md5值作对比,如果两个值不相同,则会把变量
$secure_link
设置为空字符串; - 3、如果md5值相同,nginx还会继续对比过期时间,如果链接已经过期(其实就是当前时间戳大于链接中传过来的时间戳),那么它会把
$secure_link
的值置为"0"
,否则置为"1"
; - 4、综上,我们只需要用if检测
$secure_link
的值,如果为空,则说明这个链接不是合法链接,如果不为空,则继续用if检测它的值是否为"0"
,只要为空或为0,那么我们就可以判断它不是合法请求,就可以用return 403或者rewrite的方式去处理这种不合法请求;
最终的字符串及其参数格式如下所示
http://b.test.com/fireworks.jpg?md5=WDCTPYvguH6UTEwDDqQOqQ&expires=1656830587
nginx就是通过获取到请求链接中的md5和expires值,然后与它自身的计算的md5值和当前时间进行对比,从而判断出该链接是否是有效链接。
以上原理看似简单,但实际操作的时候,还是会遇到很多问题,官方根本没有把问题说明白,我也是靠查资料才知道的,具体往下看。
具体配置文件怎么写?
主要就是使用location指令,在识别到用户访问的是静态资源文件,如图片、视频、压缩包等等文件时,使用secure_link模块的功能对它进行检测,如下所示
location ~ .*\.(jpg|jpeg|png|gif)$ {
set $secret_key "fdsfs32s*2";
secure_link $arg_md5,$arg_expires;
secure_link_md5 "$secure_link_expires$uri$remote_addr$secret_key";
if ($secure_link = "") {
return 502 "secure_link为空";
}
if ($secure_link = "0") {
return 502 "secure_link为0";
}
}
set $secret_key "fdsfs32s*2"
:就单纯设置一个变量$secret_key
,让它的值为"fdsfs32s*2"
,其实我完全可以不设置这个变量,直接把这个值写到变量的位置,只不过它需要与前面变量有个空格隔开,否则会被认为是变量名的一部分(如下所示),所以还是设置一个变量好管理一点
secure_link_md5 "$secure_link_expires$uri$remote_addr fdsfs32s*2";
secure_link $arg_md5,$arg_expires;
:用于设置你要接收的参数,把你链接中的参数前面加个$arg_
,就能接收到你的参数,比如$arg_md5
就是用来接收你请求链接中的md5
参数,$arg_expires
用于接收你链接中的expires
参数,$arg_abcd
用于接收你链接中的abcd
参数。
一般来说,md5和过期时间这两个参数是固定需要的,当然,参数名称是不限制的,比如你在链接中把md5参数叫hash,那你接收的时候就用$arg_hash
来接收就好了,你在参数中把过期时间expires
改成e
,那你接收的时候用$arg_e
接收就好了,总之参数名称不重要,重要的是你要接收到它的值。
secure_link_md5
:用于指定计算md5值的字符串,目前我这里使用了:
- 1、
$secure_link_expires
,表示过期时间,这个变量其实就是链接中的expires
参数的值,但是前面说了,用$arg_
+参数名,即$arg_expires
也能接收到这个值,所以我觉得$secure_link_expires
这个变量有点多余,不仅多余,还不实用,因为只有参数中有expires
这个参数时,$secure_link_expires
才能接收到它的值,如果你链接中的expires
改成其它名字,$secure_link_expires
就会为空,所以它很鸡肋,但我们知道,就算我改了名字,我一样可以用$arg_
来接收,比如我改成abcd
,那么我一样可以用$arg_abcd
来接收,所以用$arg_
+参数名的方式来接收链接参数值,比用$secure_link_expires
接收好多了,因为$secure_link_expires
只能接收expires
参数的值,换个名字就不行了; - 2、
$uri
,一个链接去掉协议、域名、参数,留下的部分即为uri,比如以下链接,它的uri为/fireworks.jpg
;
http://b.test.com/fireworks.jpg?md5=WDCTPYvguH6UTEwDDqQOqQ&expires=1656830587
- 3、
$remote_addr
,就是远程客户端的ip,当然如果有反代,这个ip将会固定为反代的ip,你需要注意处理这个问题,比如用$proxy_add_x_forwarded_for
来代替,这里我们暂时不考虑反代问题; - 4、
$secret_key
,就是一个自定义的字符串,一般这个自定义的字符串被称为“secret”,即密钥,其实它并没有那么高端,就是一个随意的字符串而已,长度和字符都是任意的,你自己随便写就行,只不过这个字符串复杂点可能会安全性好一点;
我是把1、2、3、4四个字符串按顺序拼在一起了,你完全可以交换它们的顺序,这个都是自己随便定的,只需要你在计算链接的md5的时候跟这个顺序一致就行,注意nginx中接字符串只要把变量放在双引号里就好了,不需要+
号和.
号。
完整可用的的nginx配置如下所示,当然你需要把root、access_log和error_log三个路径都改成你自己电脑中的路径,然后在b.test.com文件夹下放一张图片
server {
listen 80;
server_name b.test.com;
charset utf-8;
default_type text/html;
root /Users/bruce/www/personal/b.test.com;
access_log /usr/local/var/log/nginx/a.test.com.access.log;
error_log /usr/local/var/log/nginx/a.test.com.error.log;
location / {
try_files $uri $uri/ index.php$is_args$args;
}
location ~ [^/].*\.php(/|$){
fastcgi_pass 127.0.0.1:9000;
fastcgi_index index.php;
include fastcgi.conf;
}
location ~ .*\.(jpg|jpeg|png|gif)$ {
set $secret_key "fdsfs32s*2";
secure_link $arg_md5,$arg_expires;
secure_link_md5 "$secure_link_expires$uri$remote_addr$secret_key";
if ($secure_link = "") {
return 502 "secure_link为空";
}
if ($secure_link = "0") {
return 502 "secure_link为0";
}
}
}
注意nginx中的if括号里的条件,不能写成以下形式,这种语法在nginx中是会报错的
if ($secure_link = "" || $secure_link = "0"){
……
}
但是这样是不报错的
if ($secure_link || $secure_link){
……
}
也就是说,在nginx中,条件判断如果用了&&
和||
,那么各个条件必须是单独的变量,而不能是表达式。
使用php生成链接
我们以访问这张图片为例
http://b.test.com/fireworks.jpg
生成链接的php代码
// 设置过期时间为300秒
$secure_link_expires = time()+300;
// 设置当前uri
$uri = '/fireworks.jpg';
// 在真正的链接中,这个uri可以使用parse_url()函数获取
// $urlParts = parse_url('http://b.test.com/fireworks.jpg?aa=1&bb=2');
// var_dump($urlParts['path']);exit;
// 客户端ip
$remote_addr = $_SERVER['REMOTE_ADDR'];
// 密钥(就是一串自定义的字符串)
$secret_key = "fdsfs32s*2";
// 计算md5值,这里有四个地方需要注意:
// 1、参数拼接顺序必须与nginx中secure_link_md5指定的参数拼接顺序一致
$str = $secure_link_expires . $uri . $remote_addr . $secret_key;
// 2、md5第二个参数必须为true,表示返回二进制文件内容,而不是md5字符串,这个是官方文档没有提到的
$md5 = md5($str, true);
// 3、md5值需要base64编码,因为第二个参数为true它返回的不是字符串,而是二进制内容,
// 如果不用base64编码,它就是一串乱码,根本无法在url中传输,这个官方文档有说到
$md5 = base64_encode($md5);
// 4、base64编码后,得到的字符串可能会有“+-/=”这四种字符,这些符号是不能直接放到url中传输的,
// 比如+号在url中会被转为空格,从而出现问题,我们需要把+号转换成-,把/转换成_,把=号删除掉
// 而nginx那边不需要做这些符号的处理,应该是默认就按这个方式处理的吧,这个官方文档没有说到,可能是约定俗成的
$md5 = strtr($md5, '+/', '-_');
$md5 = str_replace('=', '', $md5);
// 最后输出我们的链接,注意,链接中的md5和expires两个参数,参数名不是必须这样,
// 你完全可以设置成其它参数名,只需要在nginx那边接收参数的地方修改成对应的名字即可
echo 'http://b.test.com/fireworks.jpg?md5=' . $md5 . '&expires=' . $secure_link_expires . '<br>';
最终生成的链接格式如下
http://b.test.com/fireworks.jpg?md5=rX6hH3fSu-mf76NCQs2ITg&expires=1656833493
Nginx中的secure_link $arg_md5,$arg_expires;
会接收到url中的md5
参数和expires
参数的值;
secure_link_md5 "$secure_link_expires$uri$remote_addr$secret_key";
会根据这些值计算出md5值,并把计算得到的md5值与secure_link接收到的md5值进行比对,如果不相同,则会把$secure_link
置为空字符串,这样我们就知道它是非法链接。
如果两个md5值相等,它会查看链接是否过期(通过比对当前时间戳与url中传过来的时间戳),如果过期,则会把$secure_link
置为"0"
如果不过期则会把$secure_link
置为"1"
。
所以我们只需要判断当$secure_link
为空字符串""
或为"0"
时,认为链接是非法请求,然后对这个请求进行处理即可。