关于JavaScript跨域请求的相关知识

2016 年已经过了 3 个月了,还没来得及更新博客...实在是惭愧惭愧...最近确实也很忙,一边要打工,一边要写论文,一边还要找工作...

最近百度的导出插件出了点小问题,导致很多人无法正常和 Aria2c 通讯.经过仔细的分析,发现问题出在跨域请求上,接下来就要详细说说 JavaScript 的跨域请求.

# JavaScript 跨域请求

跨域请求指得是发起请求的资源所在域不同于该请求所指向资源所在的域的 HTTP 请求.最常见的就是跨域载入图片,我们可以看到很多网站主站和网站所使用的图片 是不同的域名的.这样做的好处是请求图片的时候不会发送主站的 Cookies,因为不同域名嘛.而且还可以减少主站的服务器压力.但载入图片使用的是 GET 方法,比较简单.

# 跨域 POST 请求

我们经常使用 POST 请求发送各种指令数据,因为 POST 发送的数据没有长度限制.我们还可以在发送数据之前修改请求头,发送各种自定义的 HTTP Request Headers.但是 在跨域请求的时候 POST 的请求头被严格限制,被允许设置的请求头只有:

- Accept
- Accept-Language
- Content-Language
- Content-Type
1
2
3
4

并且允许的 Content-Type 只有一下三种:

- application/x-www-form-urlencoded
- multipart/form-data
- text/plain
1
2
3

# Preflighted requests

当我们发送的请求不满足上面的条件时,就必须发送预请求给目的站点,来查明这个跨站请求对于目的站点是不是安全可接受的。以下条件会触发预请求的发送:

  • 请求以 GET, HEAD 或者 POST 以外的方法发起请求。或者使用 POST,但请求数据为 application/x-www-form-urlencoded, multipart/form-data 或者 text/plain 以外的数据类型。比如说,用 POST 发送数据类型为 application/xml 或者 text/xml 的 XML 数据的请求。
  • 使用自定义请求头(例如 Aria2c 的验证使用的 Authorization)

# 结论

本次 BUG 的出现,是由于代码验证不规范,对于不需要用户名和密码验证的 RPC 地址.仍然发送自定义的 Authorization 验证头.导致触发 OPTIONS 请求,但是本身不支持验证的 Aria2c 客户端无法识别 OPTIONS 请求,只能返回 500 错误.导致通讯失败,至此 BUG 分析完毕.

# 参考:

HTTP access control (CORS) (opens new window)

HTTP/1.1: Method Definitions (opens new window)

突破同源策略限制的方法

# 同源策略基本介绍

同源策略(Same Origin Policy)是一种约定,它是浏览器最核心也是最基本的安全功能,如果缺少了同源策略,则浏览器的正常功能可能会受到影响。可以说 Web 是构建在同源策略的基础之上的,浏览器只是针对同源策略的一种实现。

浏览器的同源策略,限制了来自不同源的“document”或脚本,对当前“document”读取或设置某些属性。

但是随着互联网的发展跨域的需求也越来越迫切,有些时候我们需要突破同源策略。下面开始介绍突破同源策略的几种方法:

# 使用 HTTP 头

因为 JavaScript 无法控制返回的 HTTP 头,所以可以在服务器端这个 HTTP Header 即可:

self.set_header("Access-Control-Allow-Origin", "*")
1

# 使用 JSONP

Jsonp(JSON with Padding)是 json 的一种“使用模式”,可以让网页从别的网域获取资料。jsonp 是采用的 js 的回调机制来实现的。 本质上是因为 GET 方法不受同源策略限制,然后获取到的数据不是 json,而是一段 JavaScript 并解释执行。

# 使用 iframe

上面两种都是比较常见和常用的突破浏览器的方法,这种不常见的才是重点。虽然使用 iframe 算不上优雅,但也算开了眼界。在开发 115 网盘的时候,发现 115 的主站网页和获取数据的网址是不同源的,也顺便研究了一下工作原理。
主站是:http://115.com/?mode=wangpan,
但是获取数据的 API 是:http://web.api.115.com/files/download?pickcode=?
这就造成了非同源,但是 115 巧妙的使用了一个桥接网页完成了跨域操作!
桥接网址:http://web.api.115.com/bridge_2.0.html?namespace=DownBridge&api=jQuery
关键代码:

if (parent) {
  var execObj = parent.window;
  if (params['namespace']) {
    var tmp = params['namespace'].split('.'),
      f;
    while ((f = tmp.shift())) {
      execObj = execObj[f];
    }
  }
  var method = params['api'] ? params['api'] : 'DataAPI';
  execObj[method] = $;
}
1
2
3
4
5
6
7
8
9
10
11
12

可以看到,桥接是首先判断父 Window 是否存在,获取 namespace 的值设置为 execObj 最后再获取 api 的值然后设置为 execObj 的一个方法,不过这个方法对应的值肯定是 $,也就是整个 JQuery,这样我们就可以使用页面 web.api.115.com 的 JQuery 发起数据请求从而绕过跨越的限制。

下面是 115.com 上面注入的 JS 实现方法:

set_down_url:function(){
    var self=this;
    DownBridge={};
      $('<iframe>').attr('src', 'http://web.api.115.com/bridge_2.0.html?namespace=DownBridge&api=jQuery').css({
        width: 0,
        height: 0,
        border: 0,
        padding: 0,
        margin: 0,
        position: 'absolute',
        top: '-99999px'
      }).one('load',function(){
        window.DownBridge.getFileUrl=function(pickcode,callback){
        this.jQuery.get('http://web.api.115.com/files/download?pickcode=' + pickcode, function (data) {
                  callback(data);
                }, 'json');
        };
        window.DownBridge.getFileList=function(cate_id,callback){
        this.jQuery.get('http://web.api.115.com/files?aid=1&limit=1000&show_dir=1&cid=' + cate_id, function (data) {
                  callback(data);
                }, 'json');
        };
      }).appendTo('html');
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24

115 网站的这种实现方法很另类,不知道是否是有什么特殊需求导致的,但是很有意思~ 不过我还是不建议在网页里嵌入这么多的 iframe,严重影响用户体验,非常不推荐!

JavaScript处理二进制数据

最近在开发扩展的时候遇到需要获取 MP3 数据并且直接交给其它接口处理的问题,但是使用 JQuery 的 AJAX 进行 获取数据的时候已经把数据进行了编码,毕竟默认都是处理文本数据.
但是我需要的是二进制数据并且进行 Base64 编码方便进行传输,因为 Base64 编码的值全是可打印字符. Base64 常用于在通常处理文本数据的场合,表示、传输、存储一些二进制数据.

# 二进制数据转换成 Base64 编码

于是便找到了一个非常有用的 MDN 的文档Sending and Receiving Binary Data (opens new window)
不过稍作修改:

var oReq = new XMLHttpRequest();
oReq.open('GET', request.data, true);
oReq.responseType = 'blob';
oReq.onload = function(oEvent) {
  var blob = oReq.response;
  var reader = new FileReader();
  reader.onload = function(readerEvt) {
    var binaryString = readerEvt.target.result;
    // console.log(binaryString);
    var testdata = btoa(binaryString);
    var data = {
      method: 'getAudio',
      data: 'data:audio/mp3;base64,' + btoa(binaryString)
    };
    port.postMessage(data);
  };
  reader.readAsBinaryString(blob);
};

oReq.send(null);
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

设置 responseType 为Blob (opens new window) 然后使用 FileReader 作为二进制数据读取,再使用 btoa 转码成 Base64 进行传输.

还有一种是读取需要上传的文件进行处理的方式和通过 AJAX 获取的方式差不多,代码在这里 (opens new window).使用 Base64 编码的好处是在需要二进制数据的地方,例如图片和音频,我们可以使用 Base64 编码进行替换获取这些资源的 URL 可以减少 HTTP 请求,也可以做转发,我之所以有这个处理二进制数据的需求就是因为开发的一个插件无法在 HTTPS 页面上获取 HTTP 资源, 便通过 background js 进行获取数据然后 Base64 编码传输给 content js 进行使用.

# Base64 数据转换为 ArrayBuffer

有时候需要把传入的二进制数据转换成 ArrayBuffer 进行处理就需要转换了

var data = atob(base64String);
var dataView = new Uint8Array(data.length);
for (var i = 0; i < data.length; ++i) {
  dataView[i] = data.charCodeAt(i);
}
var arrayBuffer = dataView.buffer;
1
2
3
4
5
6

今天暂时就写这么多吧,期末考试看书好累,头脑都不太清醒了. QAQ