绕开 referrer 防盗链 以及服务器nodejs 作防盗链图片中转

1绕开 referrer 防盗链

最近处理了一个与referer有关的需求,发现里面还是有一点门道的。因此在本篇文章整理了referer相关知识点,主要涉及图片防盗链与如何绕开防盗链限制。

参考:
Referer-MDN

使用referer
Referer是HTTP请求头的一个字段,包含了当前请求页面的来源页面的地址,通过该字段,我们可以检测访客是从哪里来的。

那么,referer到底有啥作用呢?

交互优化
在某些web应用的交互中,右上角会提供一个返回按钮,方便用户返回上一页

其实现一般也比较简单


复制代码
这种处理方式隐藏的一个问题是:如果用户从其他入口如分享链接等地方直接进来时,点击这个按钮是无法返回。

因此在点击按钮时,我们可以判断document.referrer是否存在来优化交互:如果存在,则返回上一页;如果不存在,则直接返回首页。

应该注意到上面写的是referer,而在DOM中,使用的是referrer,这是因此请求头中的referer是由于历史原因导致的拼写错误,而在DOM规范中进行了修正,因此导致当前拼法并不统一的问题~

防盗链
当用户访问网页时,referer就是前一个网页的URL;如果是图片的话,通常指的就是图片所在的网页。当浏览器向服务器发送请求时,referer就自动携带在HTTP请求头了。

一个HTML页面往往包含多种资源,这些资源通过标签如script、img、link等形式嵌套在HTML文档中,一个完整页面往往需要经过发送多条HTTP请求下载资源,然后才能正常展示。由于HTML本身并没有对嵌套资源的来源做限制,基于这样的机制,盗链就成为了一种手段。

下面是关于盗链的百度百科定义

盗链是指服务提供商自己不提供服务的内容,通过技术手段绕过其它有利益的最终用户界面(如广告),直接在自己的网站上向最终用户提供其它服务提供商的服务内容,骗取最终用户的浏览和点击率。受益者不提供资源或提供很少的资源,而真正的服务提供商却得不到任何的收益。

打个比方:A网站将自己的静态资源如图片或视频等存放在服务器上。B网站在未经A允许的情况下,使用A网站的图片或视频资源,放置到自己的网站中。由于服务器资源是需要花钱的,这样网站B盗取了网站A的空间和流量,而A没有获取任何利益却承担了资源使用费。B盗用A资源放到自己网站的行为即为盗链。

防盗链一般由下面几种方式

定期修改文件名称或路径
通过referer,限制资源引用页的来源
通过cookie、session等进行身份认证
图片加水印等
这里我们主要关注一下referer的防盗链的原理。下面是nginx的防盗链配置

location ~* .(gif|jpg|png)$ {
valid_referers none blocked *.phptest.com;
if ($invalid_referer) {
return 403;
}
}
复制代码
这种方法是在server或者location段中加入:valid_referers。这个指令在referer头的基础上为 $invalid_referer 变量赋值,其值为0或1。如果valid_referers列表中没有Referer头的值, $invalid_referer将被设置为1。

该指令支持none和blocked,

其中none表示空的来路,也就是直接访问,比如直接在浏览器打开一个文件,
blocked表示被防火墙标记过的来路,*…com表示所有子域名。
通过referer,我们可以判断请求的来源,从而决定服务器是否正常返回请求资源,达到控制请求的目的。

需要注意的是,在某些情况下,即使用户是正常访问网页或图片,也是不会携带referer的,比如直接在浏览器地址栏直接输入资源URL,或通过浏览器新窗口打开页面等。这种访问是正常的,如果强制现在某些白名单referer名单才能访问资源,则可能误伤这一部分正常用户,这也是为什么有的防盗链检测中允许Referer头部为空通过检测的情况。

既然如此,如果把referer隐藏掉,也可以绕开部分站点防盗链的限制,下面让我们来看看如何实现隐藏referer的功能。

隐藏referer
参考

html禁用referer
以Referer方案写一个图片防盗链服务并实现网页端"破解"
在利用部分站点防盗链限制允许referer为空,或者我们仅仅是不想让服务器知道访问来源时,我们可以隐藏referer。

referrerPolicy
之前浏览器在请求资源时,会按自己的默认规则来决定是否加上Referrer。后来W3C发布了Referrer-Policy草案,运行开发者灵活地控制自己网站的referer策略。主要包含下面策略

no-referrer:任何情况下都不发送 Referrer 信息;
no-referrer-when-downgrade (默认值):在没有指定任何策略的情况下浏览器的默认行为
origin:在任何情况下,仅发送文件的源作为引用地址
origin-when-cross-origin: 对于同源的请求,会发送完整的URL作为引用地址,但是对于非同源请求仅发送文件的源。
same-origin:对于同源的请求会发送引用地址,但是对于非同源请求则不发送引用地址信息。
上面只列举了一部分可选策略,详情可参考MDN文档。

因此,我们可以手动指定no-referrer来隐藏referer

复制代码 或者在创建image对象的时候,指定referrerPolicy策略

const img = new Image()
img.referrerPolicy = ‘no-referrer’
复制代码
此时打开开发者工具就可以看见该图片的请求已经不再携带对应的referer了。总结一下,一般有下面几种设置Referer策略的方式:

通过 http 响应头中的 Referrer-Policy 字段
通过 meta 标签,name 为 referrer
通过a、area、img、iframe、link元素的 referrerpolicy 属性。
通过a、area、link元素的 rel=noreferrer 属性。
需要注意的是目前referrerPolicy仍处于提案的草稿阶段,浏览器兼容性并不是特别好。

在请求时修改header头部
XMLHttpRequest对象提供了setRequestHeader方法,用于向请求头添加或修改字段。我们能不能手动将修改 referer字段呢?

// 通过ajax下载图片
function loadImage(uri) {
return new Promise(resolve => {
let xhr = new XMLHttpRequest();
xhr.responseType = “blob”;
xhr.onload = function() {
resolve(xhr.response);
};

    xhr.open("GET", uri, true);
    xhr.setRequestHeader("Referer", ""); // 通过setRequestHeader设置header不会生效
    xhr.send();
});

}

// 将下载下来的二进制大对象数据转换成base64,然后展示在页面上
function handleBlob(blob) {
let reader = new FileReader();
reader.onload = function(evt) {
img.src = evt.target.result;
};
reader.readAsDataURL(blob);
}

const imgSrc = “http://phptest2.com/upload/1.png”;
loadImage(imgSrc).then(blob => {
handleBlob(blob);
});
复制代码
上述代码运行时会发现控制台提示错误

Refused to set unsafe header “Referer”

可以看见setRequestHeader设置referer响应头是无效的,这是由于浏览器为了安全起见,无法手动设置部分保留字段,不幸的是Referer恰好就是保留字段之一,详情列表参考Forbidden header name。

因此在通过AJAX设置referer宣告失败,那我们可以换一个方式从浏览器加载图片,比如试试Fetch呢?

Fetch是浏览器提供的一个全新的接口,用于访问和操作HTTP管道部分,该接口支持referrerPolicy,因此也可以用来操作referer。

function fetchImage(url) {
return fetch(url, {
headers: {
// “Referer”: “”, // 这里设置无效
},
method: “GET”,
mode: “cors”,
redirect: “follow”,
referrer: “” // 将referer置空,此处写成no-referrer貌似会把路径替换成 host + 'no-referrer’字符串形式
}).then(response => response.blob());
}
loadImage(imgSrc).then(blob => {
handleBlob(blob);
});
复制代码
通过将配置参数redirect置位空,可以看见本次请求已经不带referer了。

使用iframe
下面是一种通过iframe来实现隐藏referer的方式,,整个过程有点魔性。大致实现如下

const putNoRefererImage = (() => {
let iframe
/*
src: 图片地址
wrap:需要加载图片的容器
*/
return function (src, wrap) {
if (iframe) {
iframe.remove()
}

    let url = new URL(src);
    let frameid = 'frameimg' + Math.random();
    window.img = `<img id="tmpImg" width=400 src="${url}" alt="图片加载失败,请稍后再试"/> `;

    // 构造一个iframe
    iframe = document.createElement('iframe')
    iframe.id = frameid
    iframe.src = "javascript:parent.img;" // 通过内联的javascript,设置iframe的src
    // 校正iframe的尺寸,完整展示图片
    iframe.onload = function () {
        var img = iframe.contentDocument.getElementById("tmpImg")
        if (img) {
            iframe.height = img.height + 'px'
            iframe.width = img.width + 'px'
        }
    }
    iframe.width = 200
    iframe.height = 200
    iframe.scrolling = "no"
    iframe.frameBorder = "0"
    wrap.appendChild(iframe)
}

})();

putNoRefererImage(imgSrc, document.body);
复制代码
运行代码可以看见,通过这种方式也可以实现隐藏referer的功能,因此用作不支持referrerPolicy的一种替代方案。

在某些不支持javascript内联运行的场景下,这种方案也是不可行的,比如在chrome扩展程序,由于content_security_policy,使用内联JavaScript会报错

Refused to execute JavaScript URL because it violates the following Content Security Policy directive: “script-src ‘self’ blob: filesystem: chrome-extension-resource:”. Either the ‘unsafe-inline’ keyword, a hash (‘sha256-…’), or a nonce (‘nonce-…’) is required to enable inline execution.

本文总结了referer的作用,以及利用referer实现防盗链的配置。由于部分站点的防盗链配置允许referer为空,因此可以利用这一点,通过隐藏referer,来达到绕开防盗链的目的。

接着介绍了几种前端隐藏referer的实现方式,从技术上来看,referrerPolicy是近乎完美的选择,由于存在兼容性限制,因此可以通过fetch或iframe等方式来实现。

2服务器nodejs 作防盗链图片中转

怎么"破解防盗链"呢?
想要破解,就得先知道目标——防盗链如何实现。
大多数站点的策略很简单: 判断request请求头的refer是否来源于本站。若不是,拒绝访问真实图片。

而我们知道: 请求头是来自于客户端,是可伪造的。

思路
那么,我们伪造一个正确的refer来访问不就行了?
整个业务逻辑大概像这样:

自己的服务器后台接受带目标图片url参数的请求
伪造refer请求目标图片
把请求到的数据作为response返回
这就起到了图片中转的作用。

  1. 项目是什么样子
    1.1 接口的样子?
    有一个开放接口
    接口有一个参数,api?url=http://abc.com/image.png,大概长这样子
    响应内容是反防盗链后的真实图片
    1.2 应该怎么做?
    把服务器跑起来
    处理 GET 请求
    分析请求参数
    下载原图
    response 原图
  2. 学习路径(在对目标未知的前提下提出疑问)
    如何开始,建立服务器
    如何处理基本请求 GET POST
    如何下载图片并转发
    完成基本功能,上线
    优化
    2.1 如何开始,建立服务器
    主要是 http.createServer().listen(port) 这组方法,建立服务器、监听端口一键搞定。

var http = require('http');
    
http.createServer(function (request, response) {
     // do things here
}).listen(8888);
    

console.log(‘Server running at: 8888’);
2.2 如何处理基本请求 GET POST
createServer 回调方法的两个参数 req res 是 http request 和 response 的内容,打印一下他们的内容。

request 是 InComingMessage 类,打印它的 url 字段。

var http = require('http');
var url = require('url');
var util = require('util');
http.createServer(function(req, res){
    res.writeHead(200, {'Content-Type': 'text/plain'});
    res.end(util.inspect(url.parse(req.url, true)));
}).listen(3000);
请求
http://localhost:3000/api?url=http://abc.com/image.png

请求结果

Url {
  protocol: null,
  slashes: null,
  auth: null,
  host: null,
  port: null,
  hostname: null,
  hash: null,
  search: '?url=http://abc.com/image.png',
  query: { url: 'http://abc.com/image.png' },
  pathname: '/api',
  path: '/api?url=http://abc.com/image.png',
  href: '/api?url=http://abc.com/image.png' }
query 字段刚好是我们想要的内容,下载这个字段对应的图片。

2.3 如何下载图片并转发
request 模块支持管道方法,可以和 shell 的管道一样理解。

这可以省很多事,不需要在本地存储图片,不需要处理杂七杂八的事情,甚至不需要再去了解 nodejs 的流。一个方法全搞定。

关键方法: request(options).pipe(res)

var options = {
uri: imgUrl, // 这个 uri 为空时,会认为该字段不存在,报异常
headers: {
'Referer': referrer // 解决部分防盗链选项
}
};
request(options).pipe(res);

2.4 完成基本功能,上线
项目地址

完整代码

    'use strict';
    var router = require('express').Router();
    var http = require('http');
    var url = require('url');
    var util = require('util');
    var fs = require('fs');
    var callfile = require('child_process');
    var request = require('request');
    
    router.get('/', function(req, res, next) {
        var imgUrl = url.parse(req.url, true).query.url;
        console.log(url.parse(req.url,true).query); 
    
        console.log('get a request for ' + imgUrl);
        if (imgUrl == null || imgUrl == "" || imgUrl == undefined) {
            console.log('end');
            res.end();
            return;
        }
    
        var parsedUrl = url.parse(imgUrl);
        // 这里暂时使用图片服务器主机名做Referer
        var referrer = parsedUrl.protocol + '//' + parsedUrl.host; 
        console.log('referrer ' + referrer);
    
        var options = {
          uri: imgUrl,
          headers: {
             'Referer': referrer
          }
        };
    
        function callback(error, response, body) {
          if (!error && response.statusCode == 200) {
            console.log("type " + response.headers['content-type']);
          }
          res.end(response.body);
        }
    
        // request(options, callback);
        request(options)
            .on('error', function(err) {
                console.log(err)
            })
            .pipe(res);
    });
    
    module.exports = router;

2.5 优化
这部分主要是防盗链部分的优化。

单就 Referer 来说,使用空值和主机名都只能满足部分需求。

一个优化方式是组合,当一种方式不能突破即采用另一种方式。
这种方式的有点在于扩大了适用面积,并且方法对任何场景比较通用。

一个优化方式是接口请求参数带源引用连接。
这种方式对很多人来说不太通用,因为很多场景下并不清楚源引用连接在哪。
但是对我的插件来说非常适用,插件本身保留了源引用。因此可以很好的绕过防盗链限制。

发布了55 篇原创文章 · 获赞 12 · 访问量 6万+

猜你喜欢

转载自blog.csdn.net/mrzhangdulin/article/details/103526203