一种新的可应对空白 referer 的防盗链策略

目录


前言

本文介绍一种防盗链策略,可以帮助图床在收到空白 referer 请求时,仍能较准确地区分是正常请求还是盗链请求。该策略是我个人通过观察、实践、拍脑袋想出来的,目前已经在狼煞博客全面应用。实际使用效果证明,该策略可以抵御常见的反反盗链方法,但尚不确定有没有某种方法可以破解,也不确定有没有误杀的情况,故仅供读者参考。

在介绍这个方法之前,我会从头讲起我的遭遇,既然你找到这里,说明你也有过类似的遭遇,不妨读一下,应当会产生共鸣。但如果你确实时间有限,想开门见山,请跳转到强化防盗链

起因

起因?还能有啥子起因,不就是我的博客图床被人盗链了呗。

一直以来,我都没有在博客里标注过“禁止转载”或者“转载务必注明出处”等字样,也没有搞“本博客遵循 XX 国际开源许可协议,若违反协议使用本文将依法追究”什么的。不是因为我不在乎,而是因为我很清醒,这里是中国,没听说过一个小博客网站的文章被违规转载了,还能替自己维权成功的事情。

如果是君子,转载的时候一定会标明转载自哪儿,并迁移图片,防止对原博客图床造成压力;如果是小人,甚至是机器人(见《靠爬其他网站内容来充实自己——那些恶心人的网站》),它才不管那么多,哪怕你写了“未经授权转载死全家”,他也会熟视无睹,继续 Ctrl C + Ctrl V,这样一来,图片链接仍然会使用原网站的图床,于是,盗链便发生了。

我的一篇文章在我没有授权的情况下被转载了几次,被我发现了,我找过去看了一下,发现图片仍然用的我的图床,很明显是无脑复制粘贴过去的。

image

你无脑转载我管不了,但你用我的图床就别怪我心狠手辣了。咬咬牙,决定升级一下图床的防盗链策略。

我的图床

我的图床是我自己维护的,其工作原理并不复杂。

为了方便且免费实现全站 HTTPS,我放弃了使用 CDN 来加速图片,毕竟我的博客还没有那么大的访问量。目前我的博客图床用的域名是 image.wolfogre.com,这不是一个 CDN 地址,而是直接指向了我的服务器,而服务器那边做的事也很简单,就是将图片请求转发到七牛云的云存储,拿到图片返回给客户端,拓扑结构大致是:

客户端 --[https 请求]--> image.wolfogre.com --[http 请求]--> 七牛云

为了避免用户直接访问七牛云上的图片,我在七牛云上配置了 IP 白名单,只有我的服务器才能访问资源。这样一来,客户端要访问我的图片,就必须要经过我的服务器。这就是我设下的关卡,我会在这个关卡上开启 HTTPS 加密,并把每一次请求都记录到日志,这样我就有了一个平台,来检测滥用、防止滥用。

现在,我要强化这个关卡上的防盗链策略了。

首先,我做了一张图:

image

没错,我不是要单纯地让盗链的请求失败,而是要盗链的请求拿回这张魔性的图,以示警告。

针对有 referer

初步策略就是检查图片请求的 referer,这个大家都容易想到。如果不是来自 *.wolfogre.com 则重定向到魔性图片上,实际效果也很明显:

image

大家在用一些图床、云存储、CDN 服务时,想必都看到过“设置 referer 白名单”的功能,我这里的实现与它们并无不同。

不过大家也应该注意到过,这个功能有个额外选项叫“是否允许空白 referer”,正常情况下,这选项一般选“是”,因为一旦禁止空白 referer 请求,用户将不能在浏览器中直接打开图片,下载图片也有可能出现问题,所以大多数的图床都是允许空白 referer 请求的,而这便是弱点所在。

如何隐藏 referer

有没有办法隐藏请求的 referer 以实现反反盗链?有的,而且别想象的要简单。

首先要说明的是,网上有很多教程教你怎么伪造 referer,比如在代码哪儿改改,header 里加点东西什么的,就能骗过服务端了。但这不是这里要讨论的,要知道,作为客户端想怎么欺骗服务端都是可以的,但这不是盗链,而是恶意请求。这里讨论的是作为服务端,如何诱使客户端去欺骗另一个服务端,比如网站 A 诱使访问它的浏览器伪造 referer 去访问网站 B。

但可以放心,但凡是个没中过病毒的正常浏览器,是绝不会允许伪造 referer 的。

所以伪造 referer 是行不通的,只能退而求其次,选择隐藏 referer。

网上有教一种使用 iframe 来隐藏图片请求 referer 的方法,虽然有效果,但其实根本不用那么麻烦。

HTML 原生就有关闭 referer 的指令,且大多数浏览器都支持,见 Referrer-Policy

我就碰到个这么干的,当我开启了 referer 检测之后,发现仍有个盗链的网站能够正常显示图片,原因就是它在 <img> 标签里声明了 no-referrer

<img alt="image" class="has" src="http://……" referrerPolicy="no-referrer">

啧啧,厉害厉害。看到这里,我恨不得一刀切拉倒:没有 referer 的请求统统判为盗链。

事实上,如果你懒得细究,我也建议你就禁止空白 referer 访问算了,毕竟你也看到了,隐藏 referer 去盗链图片,实现起来实在太简单了。

而如果你想细究,且看我想到的一个方法。

强化防盗链

首先,空白 referer 不一定就意味着盗链,我们得理一理哪些是敌军哪些是友军,再决定是拉意大利炮还是拉意大利面。

  • 敌军:使用各种手段隐藏 referer,在非我军的网页上使用我军的图片;
  • 友军:直接用浏览器访问图片或只用其他工具下载图片。

可见,其核心区别就是图片是不是放在 <img> 标签里的,而这正是在无 referer 的情况下,甄别请求是否来自盗链的关键。

Accept 是一个鲜被关注的请求头部,用来告知服务端哪些内容类型是客户端可以处理的,且有权重的概念,表示“客户端更期待服务端返回哪种类型”,很显然,<img> 标签引起的请求“更期待返回图片”,而直接访问地址的请求则“更期待返回 HTML”(浏览器无法提前知道目标是一张图片),而使用一些工具下载图片则倾向于没有任何期待,给啥都行。

以下是我做的几个实验,可以说明这个问题。

Chrome 浏览器因 <img> 标签而发起的请求:

Host: image.wolfogre.com
Accept: image/webp,image/apng,image/*,*/*;q=0.8
Referer: https://blog.wolfogre.com/about/
User-agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/69.0.3497.92 Safari/537.36

Chrome 浏览器直接访问图片地址而发起的请求:

Host: image.wolfogre.com
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8
User-agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/69.0.3497.92 Safari/537.36

使用 curl 下载图片而发起的请求:

Host: image.wolfogre.com
Accept: */*
User-agent: curl/7.60.0

可见,相比于其他情况,因 <img> 标签而发起的请求明显地更倾向于“接收一张图片”,根据这一情况,修改后的防盗链策略为:

  1. 如果有 referer:
    1. referer 在白名单内,判定为正常请求;
    2. referer 不在白名单内,判定为盗链;
  2. 如果没有 referer:
    1. accept 内容最靠前的是“image”,则判定为盗链;
    2. 其他默认判定为正常请求。

以上只是策略的介绍,而具体怎么实施,要看图床是怎么实现的、使用的什么 HTTP 服务器。目前我的图床用的 HTTP 服务器是 Caddy,这里仅展示 Caddy 的配置,其他类型的 HTTP 服务器,比如 Nginx、Apache 可以参考实现:

https://image.wolfogre.com {
    root /opt/caddy/stc/image/ #静态文件位置,放魔性图片 anti-hotlinking.png
    redir 307 { #针对有 referer 的请求检查盗链
        if_op and
        if {path} not /anti-hotlinking.png #如果不是访问魔性图片
        if {>Referer}empty not empty #且 referer 非空
        if {>Referer} not_has wolfogre.com #且 referer 不是来自白名单
        / /anti-hotlinking.png #则重定向到魔性图片
    }
    redir 307 { #针对没有 referer 的请求检查盗链
        if_op and
        if {path} not /anti-hotlinking.png #如果不是访问魔性图片
        if {>Referer}empty is empty #且 referer 为空
        if {>Accept} starts_with image #且 accept 优先为 image
        / /anti-hotlinking.png #则重定向到魔性图片
    }
    proxy / http://cdn.image.wolfogre.com { 
        except /anti-hotlinking.png #除了访问魔性图片
        header_upstream Host cdn.image.wolfogre.com #其他访问都代理到七牛云
    }
}

误杀与漏杀

平心而论,这个策略相比于禁止空白 referer 访问,已经柔和了很多,只是在默认允许空白 referer 访问的基础上,针对特殊情况增加了防护策略,所以个人认为其误杀的可能性较低。

漏杀则是有可能的,如果用户使用的浏览器并没有按权重提供 Accept,服务端就可能将盗链误判为正常请求。但只要只要针对绝大多数浏览器能够正常判定为盗链,对盗链者来说已经是致命的打击了。

而这方法有没有其他漏洞,尚不得而知,我会持续观察日志,看有没有缺乏考虑的地方。

我的前端知识很有限,所以真心不敢保证这个策略能胜任在生产环境,所以如果读者深谙前端,发现了这个策略的破绽,欢迎指出,一起讨论。