Saber 酱的抱枕

Fly me to the moon

05/23
2017
学习 软件

使用GM_xmlhttpRequest设置referer获取文件

说到ajax,不能不提到跨域限制这个问题。与跨域紧密相连的是referer,如果referer相同,就不会触发跨域限制;反之则视为跨域。

但发生跨域时,能否请求到跨域资源,也分两种情况:

第一种:服务器端不检查referer,这时候是否受跨域限制,由浏览器管理。

这时是否触发跨域限制,全由浏览器自己判断。比如在网页上用img标签插入一张跨域的图片,浏览器是不会触发跨域限制的,可以正常加载图片。但用ajax获取的话,浏览器就会触发跨域限制,获取不了。

这时候就算跨域了,服务器也是允许浏览器去下载文件的,只是看浏览器自己愿不愿意。

第二种:服务器端会检查referer,不符合规则的就拒绝。

这时候浏览器就吃瘪了,用img标签来插入图片也不行,这也是通常的防盗链手段。


如果我们在请求头里设置合法的referer,就可以破解跨域限制,但目前运行在浏览器宿主环境内的JavaScript实现不了这个功能,需要交给后台程序来做。如果只为了破解referer就单独做个后台文件,比较麻烦。但是我们使用油猴脚本的话,就不用写后台文件了,比较省事。

油猴脚本管理器(Greasemonkey或Tampermonkey等)由于是浏览器的扩展程序,可以设置请求头,包括伪造referer。它们都封装了一个GM_xmlhttpRequest方法,可以在用户脚本(UserScript)里调用。

今天我进行了测试,确定是可行的。示例代码如下:

// ==UserScript==
// @name        test GM_xmlhttpRequest
// @description test
// @namespace   https://greasyfork.org/ja/users/24052-granony
// @author      me
// @version     0.1
// @include     https://greasyfork.org/*
// @grant       GM_xmlhttpRequest
// @connect     i.pximg.net
// @connect     i1.pixiv.net
// @connect     i2.pixiv.net
// @connect     i3.pixiv.net
// @connect     i4.pixiv.net
// @connect     i5.pixiv.net
// ==/UserScript==

GM_xmlhttpRequest({
	method: "GET",
	url: "https://i.pximg.net/img-original/img/2017/05/16/00/20/10/62921231_p0.png",
	headers: {
		referer: "https://www.pixiv.net/"
	},
	overrideMimeType: "text/plain; charset=x-user-defined",
	onprogress: function(xhr) {
		console.log(xhr.lengthComputable + "," + xhr.loaded + "," + xhr.total);
		//xhr.lengthComputable:布尔值,是否可以获取到文件总长度
		// xhr.loaded:已加载的字节数
		// xhr.total:文件总字节数

	},
	onload: function(xhr) {
		var r = xhr.responseText,
			data = new Uint8Array(r.length),
			i = 0;
		while (i < r.length) {
			data[i] = r.charCodeAt(i);
			i++;
		}
		blob = new Blob([data], {
			type: "image/png"
		});
		var blobURL = window.URL.createObjectURL(blob);
		var downA = document.querySelector("h1 a");
		downA.href = blobURL;
		downA.setAttribute("download", "a.png");
		downA.click();
		window.URL.revokeObjectURL(blobURL);
	}
})

上面代码的功能是下载一个跨域并且服务器设置了referer防盗链的文件,下载后将其转换为blob对象保存到本地。

首先要授予这个脚本调用GM_xmlhttpRequest的权限:

// @grant       GM_xmlhttpRequest

但光这样还不够,还要在@connect里指定跨域获取文件时文件url里的域名。如果文件url的域名没有在@connect里指定,则GM_xmlhttpRequest会报错。

如果跨域文件没有设置referer防盗链,那么到这里就够了,跨域问题会被自动处理(。

但示例代码里的url有防盗链设置,而且比较奇葩,服务器要求的其实是个不同源的referer,同源的referer反而不行。这就需要我们在GM_xmlhttpRequest方法的headers参数里设置合法的referer了:

headers: {
	referer: "https://www.pixiv.net/"
}

其实上面的代码里还有个挺纠结的地方,就是把接收的数据转换为blob对象的过程。

JavaScript原生的XMLHttpRequest对象和jQuery的ajax方法都可以设置把接收的数据自动转换为blob类型,如:

xhr.responseType="blob";

这样拿到的response直接就是blob对象,但这个办法油猴的GM_xmlhttpRequest里测试不行。我见到有的油猴脚本在GM_xmlhttpRequest里设置了:

responseType: "blob",

我试了也不行,不知道人家是怎么用的,反正我这里测试是不行……

后来我在github上找到了一个办法可以把接收的数据转换为blob对象,就是上面代码里用的,看着挺费劲。主要是做了三个工作:

1:请求前设置overrideMimeType;
2:onload之后用Uint8Array和charCodeAt将数据正确的转换为blob对象。

Uint8Array和charCodeAt和这俩我之前完全不认识,看到的时候一脸懵逼:

后来我深入了解了上面步骤的作用:

overrideMimeType告诉服务器需要返回无格式纯文本的mime-type,而不是image/jpeg、image/png等图像格式。

Uint8Array是创建一个指定长度的无符号数组,charCodeAt则用来把response逐字节转换为Unicode编码(response都是string)。

怪不得我之前简单粗暴把response放进数组里转换成的blob对象有问题,还是姿势太低。

其他参考资料:Greasemonkey wiki

使用GM_xmlhttpRequest设置referer获取文件