Skip to content

[nginx/openresty]代理请求修改referer,破解防盗链

有时我们想引用其他网站的资源(图片,视频等),明明在其网站上可用,而我们发送请求时却得到了403错误,访问被拒绝,很可能就是该网站对这些资源文件设置了防盗链,下面我们聊聊其防盗的原理以及破解方法。

名词

防盗链

  • 其效果顾名思义,就是希望阻拦其他网站来使用自己的资源文件,因为这种资源引用往往对于原本的网站来说没有收益,却还要承担流量,嫁衣了属于是。因此我们可以加一些判断,当请求不是自己允许的网站发来的时候,就拒绝。

  • 而这个判断依据大多就是Http请求头中的Referer属性

Referer

  • 当浏览器发送请求时,一般会带上Referer,告诉服务器我是哪个页面发送过来的,服务器借此可以获得一些信息用于处理。

  • 在浏览器中,我们不能自定义请求的Referer属性,它是由浏览器给请求带上的。

  • 当我们在地址栏直接输入网址并回车时,会发送一个Get请求,并且其Referer属性为空

  • 到这里,相信你已经发现防盗链为什么使用Referer来判断,正是因为在浏览器中,它不能被伪造。

Origin

  • 在请求头中,用于指明当前请求来自于哪个站点,仅包含站点信息,不包含任何路径信息

  • 服务器端也经常使用这个属性来判断是否允许请求。

  • 而行为正确的浏览器,也会用这个属性判断是否允许跨域请求。

破解防盗链

  • 请求的所有属性,我们实际上都可以伪造,但伪造了不允许自定义的属性后的请求放在浏览器中是发不出去。

    • 如果我们是客户端(比如exe,app)而不是网页,不使用浏览器,自己发送请求则可以随意伪造。

    • 对于网页,我们也是有办法的。这就需要让一个服务器来代理,网页把请求发到我们的服务器,由服务器去伪造请求,拿到我们要的资源后再返回给网页即可。

  • 下面我们将从这两种情况出发,以请求b站接口示例,给出解决方法。

直接伪造请求

这个方法适用于客户端,而不适用于浏览器中的网页。

  • 绝大多数编程语言(c++,java,js)都有自己热门的网络库,都应该会支持自定义请求头

  • 这里我们使用js来演示,注意这里的js是运行在node环境的,而非浏览器环境

    • 安装好node环境

    • 新建一个文件夹

    • 打开cmd,cd路径到这个文件夹内,执行npm install axios安装axios

    • 在这个文件夹里面新建一个文件test.js,写入:

js
const axios = require(`axios`);
const getServer = (in_url, in_data, successFun, errFun) => {
    axios({
        method: "get",
        url:    in_url,
        params: in_data,
        headers: {
            "referer":"https://www.bilibili.com/"

            //"referer":"https://127.0.0.1/"  //你可以尝试使用这一句替换上面那句,请求将被拒绝
        }
    }).then(function (res) {
        console.log(res.data);
        if (typeof (successFun) != 'undefined') {
            successFun(res);
        }
        console.log("--- 请求成功 ---");
    }).catch(function (err) {
        console.log(err);
        if (typeof (errFun) != 'undefined') {
            errFun(err);
        }
        console.log("--- 请求失败 ---");
    })
}

//获取对应id的视频的下载链接
getServer(
    "https://api.bilibili.com/x/player/playurl",
    {
        "fnval": 80,
        "bvid":"BV1pT41157it",
        "cid":"746904707"
    });
  • 这个b站接口可以获取到对应视频的下载链接

  • 其referer允许置空,允许是“https://www.bilibili.com”等b站设置的允许的网站

  • 你可以尝试伪造referer为"http://127.0.0.1/",模拟在浏览器中从127.0.0.1的网页中发出请求,我们就会发现请求错误,被拒绝了。

  • 因此我们上面直接伪造为b站首页地址去请求是相当成功的。

代理转发

客户端,网页都适用,但一般客户端不需要由服务器代理,客户端自己可以搞定,就不需要占用服务器的带宽性能

  • 这里我们的服务端使用openresty(nginx)转发请求,配置nginx的配置信息去修改referer等即可

  • 示例:

conf
server {
        listen       80;
        server_name  localhost, 127.0.0.1;
	resolver 8.8.8.8;
        location / {
		header_filter_by_lua_file /usr/local/openresty/lua/toBili.lua;
                proxy_set_header referer 'https://www.bilibili.com/';
		proxy_set_header Host $proxy_host;
		proxy_set_header Origin 'https://www.bilibili.com';
		if ($query_string ~* ^(.*)url=(.*)$) {
			proxy_pass $2;
		}
	}
}
  • -
    • 其中用到的lua脚本文件,toBili.lua:

    • 这个脚本是修改响应头的3个属性,让请求允许跨域

conf
ngx.header['Access-Control-Allow-Origin']  =  '*';
ngx.header['Access-Control-Allow-Methods'] = 'get, post, options';
ngx.header['Access-Control-Allow-Headers'] = 'dnt,x-mx-reqtoken,keep-alive,user-agent,x-requested-with,if-modified-since,cache-control,content-type,authorization';
  • 下面我们来一步步分析配置文件和lua代码

  • 显然这个配置文件是给http://127.0.0.1用的

  • 第四行:resolver 8.8.8.8;

    • 如果这一句不加,当转发目的地址是一个域名时,请求会出错,服务器返回502
  • 在location / {} 中

关于空referer

  • 伪造空referer
    • 前面我们提到,之前在浏览器敲地址后进入,其请求的referer就是空的

    • 除了用服务器代理置空以外,我们还可以在html网页中配置meta标签,告诉浏览器在跳转页面时不要带上referer

      • 在html的head标签内增加:<meta name="referrer" content="never">

      • 还有可以设置单个<a>标签跳转时不带上referer、使用window.open打开等操作。

html
<!DOCTYPE html>
<html lang="zh">
<head>

    <meta name="referrer" content="never">
</head>
<body>
</body>
  • 然而部分浏览器在使用上面的方法跳转下载文件时,由于其下载加速之类的功能,将会自动截取下载链接中的域名等添加为referer。
    • 这么做初衷当然是好的,然而目前很多网站会使用云服务器商(如阿里云、腾讯云等)提供的对象存储、cdn等,而且这些功能提供的下载链接大部分的域名和源网站并不相同,但他们会拦截非源网站的referer,甚至有的自己的域名referer都拦截!

    • 例如:

      • 假设我想在我的网站提供下载B站视频,假设b站视频由cdn提供下载,且仅允许b站referer和空referer

        • 我们的网站是:www.coolight.cool

        • b站:www.bilibili.com

        • cdn提供的某个视频的下载链接:www.cdn.com/bilibili/video/BV123456789

      • 当我们使用上述有下载加速功能的浏览器时,首先在我们的网站,点击下载按钮,然后跳转到下载链接[https://www.cdn.com/bilibili/video/BV123456789\],此时应是空referer,但下载加速功能自动截取下载链接的域名部分作为referer[https://www.cdn.com/\],然后发送请求,而cdn发现请求referer即非空,也非b站,则会返回403拒绝请求。

      • 最死的还是有些浏览器,先发第一个请求获取目标下载文件的大小等信息,这个请求确实是没有referer,所以成功得到文件信息,然后开始分块下载,这个时候又带上自动截取的referer,然后就导致后面下载失败。

    • 解决办法:

      • 提示建议用户更换浏览器

      • 提示建议用户关闭浏览器的下载加速之类的功能

      • 更换使用服务器代理

摸索过程记录

  • 难点显然是在给web的代理转发上,一开始懂的也很少,还精准踩了很多百度上没有的坑。

  • 首先是proxy_set_header referer 'https://www.bilibili.com/';

    • 一开始就写了这句,然后就直接proxy_pass $arg_url转发,结果就一直返回502错误。

    • 百度才发现需要加一句resolver 8.8.8.8

  • 然后先是尝试了转发到百度,也就是写 http://127.0.0.1/?url=https://www.baidu.com,正常可用!

  • 但在尝试b站的接口的时候,有的接口可以,有的却不行,这个时候我就开始疑惑

  • 后来直接把proxy_pass的目标路径改成一个固定链接 https://api.bilibili.com/x/player/pagelist?bvid=BV1pT41157it,居然成功了,那显然问题就是这个$arg\_url

  • 然后就发现了它在目标地址携带多个参数时截取后会丢失部分参数

  • 修改完这个问题后,在浏览器地址栏直接测试都是成功的了

  • 接下来就是把这个方法用在项目里,出现跨域问题

  • 配置跨域

    • 自然而然地百度后直接修改配置文件,增加三句:
conf
add_header Access-Control-Allow-Origin '*';
add_header Access-Control-Allow-Methods 'POST,OPTIONS,GET';
add_header Access-Control-Allow-Headers 'DNT,X-Mx-ReqToken,Keep-Alive,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Authorization';
  • -
    • 但是后来发现有些接口它自己会添加Access-Control-Allow-Origin,然后我配置了nginx也添加了这个,导致这个的值出现多次,浏览器开始报错

    • 于是寻找方法,希望可以判断是否存在,没有则添加,有则修改

    • 于是找到了openresty,可以认为它是 nginx + 插件

    • 然后在openresty配置了lua脚本去修改。

思考

上面的示例中,我们在使用时,只能也必须传入一个参数 url,那有没有办法传递更多参数呢?

  • 1: 可以将参数 url 的值进行 url编码,避免歧义,然后在nginx中截取出参数url的值后进行 url解码后再转发请求即可。

  • 2: 链接的路径部分我们还可以动动手脚。