防盗链的矛与盾

大家好,我是雷布斯。

大家有没有注意到,我们官网(https://fe.ecool.fun/)“面经”、“技术文章”栏目下的文章,都已经支持本站直接访问,而之前可是需要跳转到微信公众号的,大家知道是怎么实现的吗?

这里面其实有两个关键技术点:“文章数据获取”以及“文章中的图片资源绕过防盗链限制”,具体的技术细节,计划会在后期单独出一篇文章,咱们今天先来看看“防盗链”这块的基础知识。

1. 盗链是什么?

盗链是指将其他网站上的图片、视频或其他媒体文件,显示到自己的网页上。

这种行为通常会给被链接的网站带来额外的带宽消耗和资源浪费,而且可能侵犯了原始网站的版权。

2. 为什么会有盗链?

假设有这样一个场景:前端同学 A 想开发一个技术博客,用来记录自己的技术成长历程。他购买了一台云服务器,并使用开源的博客框架进行了部署。

但文章中有很多图片,而服务器的带宽配置较低,图片加载很慢,你有什么好办法吗?

从技术的角度来看,可以通过购买内容存储服务(如对象存储服务 - OSS),来实现对图片资源的集中存储。同时,开启 CDN(内容分发网络)服务,可以实现对静态资源的快速访问加速。

那有没有更简单而且免费的方案呢?

大部分人可能会在查找资料后,会选择“免费版图床”!

图床(Image Hosting Service)是一种在线服务,它允许用户将图片上传到互联网上的服务器,并生成图片的外部链接(URL)。这个外部链接可以在网页、社交媒体、论坛等地方使用,以展示或分享图片。图床通常用于在网页上显示图片,以减轻网站服务器的负担,同时提高页面加载速度。

图床的主要优势是避免在网站服务器上存储大量图片文件,从而节省存储空间和带宽。此外,图床还可以提供一些额外的功能,如图片压缩、管理、分享、编辑等。

以我之前使用过的 Chrome 的插件“即刻图床”(现已被下架)为例,上传图片后,即可获取对应的 CDN 访问链接。

图床的用户界面
图床的配置页面

我们可以发现,这个插件可以提供“58 同城”、“京东”等网站的图床及 CDN 服务,本质上,这种图床属于“盗链”。

那么为什么会出现盗链?

  • 一方面是因为很多人不想承担资源存储和带宽的成本
  • 另一方面也是因为对应的厂商存在技术漏洞或者防护措施太过薄弱。

3. 盗链与防盗链的博弈

我们所说的“防盗链”,其实就是与盗链者的博弈。

本文主要围绕图片的“盗链”与“防盗链”展开。

3.1 Round 1 - 搭建一个免费图床与设置可访问的 referer

3.1.1 盗链者

所谓的“免费图床”,大部分都是盗用其他公司的 OSS 及 CDN 资源。

大部分的网站或者 App 基本都有图片上传的功能,比如最常见的用户头像上传。开发者完全可以模拟正常用户,将个人网站上的图片上传请求转发到其他的网站,拿到返回的 CDN 地址后,用于自己的网站。

以京东的“评价晒单”为例(仅供技术学习):

通过上传一张图片,我们可以看到对应的上传请求,通过直接复制 Node.js 的请求链接,我们就拿到了一个可以直接在 Node.js 中使用的请求方法。

将请求体中的数据换成自己的图片,然后对外提供一个上传的转发接口,就能依托京东,实现了一个简易版本的图床。当然,这种方案会有一些缺点,比如登录态需要定期更新。

fetch("https://club.jd.com/myJdcomments/ajaxUploadImage.action", {
  "headers": {
    "accept""*/*",
    "accept-language""zh-CN,zh;q=0.9,en;q=0.8",
    "cache-control""no-cache",
    "content-type""multipart/form-data; boundary=----WebKitFormBoundaryRE3eavlaBJiyXn9Q",
    "pragma""no-cache",
    "sec-ch-ua""\"Not_A Brand\";v=\"8\", \"Chromium\";v=\"120\", \"Google Chrome\";v=\"120\"",
    "sec-ch-ua-mobile""?0",
    "sec-ch-ua-platform""\"macOS\"",
    "sec-fetch-dest""empty",
    "sec-fetch-mode""cors",
    "sec-fetch-site""same-origin",
    "cookie""xxxx",
    "Referer""https://club.jd.com/myJdcomments/orderVoucher.action?ruleid=xxxx",
    "Referrer-Policy""strict-origin-when-cross-origin"
  },
  "body""------WebKitFormBoundaryRE3eavlaBJiyXn9Q\r\nContent-Disposition: form-data; name=\"name\"\r\n\r\npolicy-1.png\r\n------WebKitFormBoundaryRE3eavlaBJiyXn9Q\r\nContent-Disposition: form-data; name=\"PHPSESSID\"\r\n\r\nmvpjl6muuk705ipboi3ia0b461\r\n------WebKitFormBoundaryRE3eavlaBJiyXn9Q\r\nContent-Disposition: form-data; name=\"Filedata\"; filename=\"policy-1.png\"\r\nContent-Type: image/png\r\n\r\n\r\n------WebKitFormBoundaryRE3eavlaBJiyXn9Q--\r\n",
  "method""POST"
});

最后将接口返回的 CDN 链接用于个人网站,就实现了最简单的盗链。

3.1.2 防盗链

下面我们来看下怎么防盗链,首先需要知道图片的请求是从哪里发出的。

可以实现这一功能的有请求头中的 originreferer

origin 表示当前请求资源所在页面的协议和域名,用来说明请求从哪里发起的,但是一般在 CORS 跨域请求中才会带上,普通请求中没有。

带 origin 的请求头

而referer 记录当前请求资源所在页面的完整路径,我们一般可以使用 Referer 进行最简单的过滤,也就是仅支持指定的 referer 访问我们的资源。

阿里云 OSS 的防盗链配置页面

正确英语拼写应该是 referrer,由于早期 HTTP 规范的拼写错误,为了保持向后兼容就一直延续下来

对于非法访问,一般会返回默认图或者 403

如果是自己的服务器,可以直接通过代理服务器进行配置,以 Nginx 的配置为例:

location ~.*\.(gif|jpg|png|flv|swf|rar|zip)$
{
valid_referers none blocked test.com *.test.com; //加none的目的是确保浏览器可以直接访问资源
if($invalid_referer)
{
#return 403; // 直接返回403
rewrite ^/ http://www.test.com/403.jpg;//返回指定提示图片
}
}

3.2 Round 2 - 空 referer 与更精细的过滤策略

3.2.1 盗链者

上面的阿里云防盗链页面中,有个额外选项叫“是否允许空 Referer”,正常情况下,这选项一般选“允许”,因为一旦禁止空白 referer 请求,用户将不能在浏览器中直接打开图片,下载图片也有可能出现问题,所以大多数的图床都是允许空白 referer 请求的,而这便是弱点所在。

直接访问掘金图片的请求头

假如我给页面的 referrer 设置成 never,或者将图片的 referrer 单独设置 no-referrer,图片请求时就不会在请求头中带上 referer

<meta name="referrer" content=“never”>
// or 
<img referrer="no-referrer" src="xxxx"/>

referrer 的取值及含义,大家可以参考 MDN。

PS: 在 Chrome 中,即使页面上设置 <meta name="referrer" content=“never”>,background-image 中的图片请求还是会带上 referer,但火狐中不会带上。

3.2.2 防盗链

从防盗链的角度,怎么针对空白 referer 进行优化呢?

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

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

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

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

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

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

Accept: image/webp,image/apng,image/*,*/*;q=0.8
Referer: https://xxx.xxx.com/
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 浏览器直接访问图片地址而发起的请求:

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 下载图片而发起的请求:

Accept: */*
User-agent: curl/7.60.0

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

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

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

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

其他不带 referer ,可能会引发误判的场景:

  • 在部分开启了隐私模式的浏览器上,referer 就是空
  • 在 https 的页面上请求 http 的图片,也不会携带 referer

3.3 Round 3 - 资源转发与 Token 签名

3.3.1 盗链者

如果按上面的方法配置,盗链者还有办法吗?

还真是有,那就是做一层资源的转发,在转发服务中,直接设置请求头中的 referer

以 Node.js 服务为例:

const express = require('express');
const request = require('request');

const app = express();
const port = 3000;

app.use((req, res, next) => {
  // 设置允许跨域访问
  res.header('Access-Control-Allow-Origin''*');
  next();
});

app.get('/proxy', (req, res) => {
  const { url } = req.query;

  if (!url) {
    return res.status(400).send('Missing "url" parameter');
  }

  const headers = {
    // 设置 Referer 头部,可以使用你的网站 URL 或其他合适的值
    'Referer''https://yourwebsite.com',
  };

  // 发起对第三方资源的请求
  request({ url, headers }, (error, response, body) => {
    if (error) {
      return res.status(500).send('Error fetching the resource');
    }

    // 将第三方资源的响应直接转发给客户端
    res.send(body);
  });
});

app.listen(port, () => {
  console.log(`Server is running on port ${port}`);
});

在使用时,只需要在原来 url 前增加自己的转发服务链接,比如 https://xxx.xx.com/proxy?url=xxxxx

不过这种方式需要利用个人服务器进行转发,会占用机器的带宽和流量。而且如果目标图片太大,比如有几十M ,那么带宽小的服务器肯定是吃不消的,加载速度会特别慢。

同时,我们也得避免自己的接口被别人盗用。

当然也有一些免费的转发服务,但是访问速度很不稳定,比如:

<img src=”https://images.weserv.nl/?url=https://img3.doubanio.com/view/photo/s_ratio_poster/public/p2544866651.jpg" />

3.3.2 防盗链

那现在我们还有什么方法来防御吗?

可能有同学会想到 IP 禁用,针对访问量比较大的 ip,直接封禁掉,但对于访问量不大的网站就不起作用了。

还有同学会想到 User-Agent 防盗链,使用定制的 User-Agent 才能正常访问,但还是无法避免资源转发时自定义 User-Agent,而且这种方式一般只用在 App 上。

其实还有一个大招:URL 参数签名,也就是我们常说的 Token 防盗链

以“又拍云”的 Token 防盗链功能为例:

如上图所示,整个 token 防盗链的实现需要如下几个部分来配合:

  • 客户端:负责发送原始请求给客户端业务服务器以及发送带签名的 URL 给 CDN 节点进行验证;
  • 业务服务器:根据约定的算法生成带 _upt 参数的 URL 返回给客户端;
  • CDN 节点:负责和客户端进行时间、签名校验;

原理也很简单,就是在资源的 url 中带上一个 token,token 中会对当前链接、过期时间等信息进行签名,然后 CDN 节点在处理请求时,先校验 token,如果遇到已过期或者其他异常场景,就禁止访问。

3.4 小结

看完上述的介绍,大家会发现在防盗链的博弈中,没有绝对的优势方。

这是一个不断演进和变化的过程,双方都在不断寻找新的方法来保护或者绕过防护措施。

就比如 token 防盗链就一定安全吗?其实也未必,比如盗链者完全可以定期更新图片链接,而作为防盗链的一方,我们则是可以使用更加完善的流量监控系统。

大家也需要明白,所有的防盗链措施,都是为了提高盗链的成本。

4. 最后

还没有使用过我们刷题网站(https://fe.ecool.fun/)或者刷题小程序的同学,如果近期准备或者正在找工作,千万不要错过,我们的题库主打无广告和更新快哦~。

老规矩,也给我们团队的辅导服务打个广告。