orangeyyy
12/29/2017 - 11:40 AM

跨越访问

同源策略

浏览器出于安全考虑提出了同源策略,来阻止一个域下面的资源与另一个域下面的资源进行通信,可以说同源策略是浏览器安全的基石。跨域访问就是为了绕过浏览器的同源策略来进行通信,因此,要想进行跨域访问最好先了解一下同源策略。

何为同源

  • 协议相同;
  • 域名相同;
  • 端口相同;

举个简单的例子:http://www.example.com/dir/index.html

  • 协议:http://
  • 域名:www.example.com
  • 端口号:80(默认端口号省略)

a.example.com 和 b.example.com属于不同源,因为域名不同;

限制范围

跨域情况下浏览器做了如下限制:

  • Cookie、LocalStorage 和 IndexDB 无法读取;
  • Dom无法访问;
  • Ajax请求无法发送;

支持跨域的资源

浏览器也不是限制了页面的所有的资源跨域,以下资源就可以跨域访问:

  • <script src="..."></script>标签嵌入跨域脚本。语法错误信息只能在同源脚本中捕捉到;
  • <link rel="stylesheet" href="...">标签嵌入CSS;
  • <img>嵌入图片。支持的图片格式包括PNG,JPEG,GIF,BMP,SVG,...;
  • <video><audio>嵌入多媒体资源;
  • <object>, <embed><applet>的插件;
  • 跨域访问@font-face引入的字体。一些浏览器允许跨域字体( cross-origin fonts),一些需要同源字体(same-origin fonts);
  • <frame><iframe>载入的任何资源。站点可以使用X-Frame-Options消息头来阻止这种形式的跨域交互;

跨域访问

接下来会针对上面提出的几种同源限制分别介绍绕过同源策略的跨域访问方法:

Cookie

Cookie 是服务器写入浏览器的一小段信息,只有同源的网页才能共享。

document.domain

对于两个网页以及域名相同,只有二级域名不同,浏览器允许通过设置document.domain共享Cookie。

举栗说明: 现在有A页面a.example.com和B页面b.example.com,我们只要如下设置相同的域名就可以共享Cookie了:

document.domain = 'example.com';

如果A页面设置cookie:

document.cookie = 'hello';

B页面可以读到A页面设置的cookie:

let cookie = document.cookie;

LocalStorage和IndexDB无法通过这种方式绕开同源策略;

服务器端也可以在设置Cookie时指定Cookie所属域名为一级域名(例如.example.com),那么二级、三级域名不需要做任何设置都可以访问这个Cookie;

通过上面的设置页面将会成功地通过对 http://example.com的同源检测,因此如果设置document.domain = 'otherExample.com'是不起作用的;

Iframe

如果两个网页不同源,就无法拿到对方的DOM。典型的例子是iframe窗口和window.open方法打开的窗口,它们与父窗口无法通信。

举个栗子,若页面与其嵌套的iframe不同源: 父容器访问子iframe

document.getElementById("myIFrame").contentWindow.document
// Uncaught DOMException: Blocked a frame from accessing a cross-origin frame.

子iframe访问父容器

window.parent.document.body
// 报错

document.domain

对于一级域名相同的两个页面,我们可以设置相同的document.domain来规避同源策略,访问DOM。具体方法如上一节所示。

location.hash

hash值是URL中#后面的部分,改变URL的#不会引起页面的重新刷新。我们可以通过hash值来进行父子窗口之间的通讯:

  • 父容器向子窗口发消息:
var src = originURL + '#' + data;
document.getElementById('myIFrame').src = src;
  • 子窗口向父窗口发消息:
parent.location.href= target + "#" + hash;
  • hash值监控:
window.onhashchange = () => {
  let hash = location.hash;
}

PostMessage

window.postMessage(message, targetOrigin) 方法是 html5 标准新引进的特性,可以使用它来向其它的 window 对象发送消息,无论这个 window 对象是属于同源或不同源。兼容性:

发送消息:

targetWindow.postMessage(message, targetOrigin, [transfer]);
  • targetWindow: 接收消息的window实例,我们可以通过下面的方法来获取这个实例:
    • window.open(url):打开一个新的窗口并获取新窗口的window实例;
    • window.opener:被打开的窗口获取执行打开操作的窗口的window实例,只有通过window.open打开的窗口才会有;
    • HTMLIFrameElement.contentWindow:获取嵌入的iframe的window实例;
    • window.parent:在iframe中获取父容器的window实例;
    • window.frames:根据name或者index获取window实例;
  • message:需要发送的消息,可以直接传入一个对象;
  • targetOrigin:设置接收消息的窗口的源,即“协议+域名+端口号”,例如“http://www.example.com“,也可设置为“*”,表示不限制源;
  • transfer:伴随在message中的可转移(Transferable)对象序列,一般用不到;

接收消息:

window.addEventListener("message", (event) => {
  
}, false);

event中主要包含以下信息:

  • data: postmessage发送的消息内容;
  • origin:发送消息的窗口的源;
  • source:发送消息的窗口的window实例;

window.name

浏览器窗口有window.name属性。这个属性的最大特点是,在一个窗口(window)的生命周期内,窗口载入的所有的页面都是共享一个 window.name 的,每个页面对 window.name 都有读写的权限,window.name 是持久存在一个窗口载入过的所有页面中的,并不会因新页面的载入而进行重置。 举个简单的栗子: A页面http://www.example.com/a.html想获取B页面http://www.otherexample.com/b.html中的数据。 B页面的逻辑非常简单,只需要把数据放到window.name中:

window.name = data;

A页面需要用一个隐藏的iframe来做中间角色,由iframe去获取B页面数据,然后A页面在通过iframe去获取数据:

// a.html
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title>Document</title>
  <script>
    function getData() {
        var iframe =document.getElementById('iframe');
        iframe.onload = function() {
            var data = iframe.contentWindow.name; // 得到
        }
        iframe.src = 'c.html';  // c.html必须与A页面同源
    }
  </script>
</head>
<body>
  <iframe src="https://www.qiutc.com/data.html" style="display:none" onload="getData()"</iframe>
</body>
</html>

这种方法的优点是,window.name容量很大(2M),可以放置非常长的字符串;缺点是必须监听子窗口window.name属性的变化,影响网页性能。

AJAX

同源政策规定,AJAX请求只能发给同源的网址,否则就报错。我们通过下面方式来进行跨域访问:

JSONP

JSONP是服务器与客户端跨源通信的常用方法。最大特点就是简单适用,老式浏览器全部支持,服务器改造非常小。

它的基本思想是,网页通过添加一个<script>元素,向服务器请求JSON数据,这种做法不受同源政策限制;服务器收到请求后,将数据放在一个指定名字的回调函数里传回来。

由于<script>元素请求的脚本,直接作为代码运行。这时,只要浏览器定义了foo函数,该函数就会立即调用。作为参数的JSON数据被视为JavaScript对象,而不是字符串,因此避免了使用JSON.parse的步骤。

WebSocket

WebSocket是一种通信协议,使用ws://(非加密)和wss://(加密)作为协议前缀。该协议不实行同源政策,只要服务器支持,就可以通过它进行跨源通信。

CORS

CORS是跨源资源分享(Cross-Origin Resource Sharing)的缩写。它是W3C标准,是跨源AJAX请求的根本解决方法。相比JSONP只能发GET请求,CORS允许任何类型的请求。

CORS需要服务端和浏览器同时支持才能够正常工作,目前所有的主流的浏览器已经支持了CORS(IE到11才完全支持)。

整个CORS通信过程,都是浏览器自动完成,不需要用户参与。对于开发者来说,CORS通信与同源的AJAX通信没有差别,代码完全一样。浏览器一旦发现AJAX请求跨源,就会自动添加一些附加的头信息,有时还会多出一次附加的请求,但用户不会有感觉。

综上可以看出,CORS通讯的关键在服务端,只要服务端支持了CORS通讯,那么就可以进行跨域访问了。

请求类型

浏览器将CORS访问分为两类:简单请求和非简单请求。

  • 简单请求:同时满足下面两个条件:
    • 请求方法为下面三种之一:HEAD,GET,POST;
    • HTTP头不超出以下几种字段:
      • Accept;
      • Accept-Language;
      • Content—Language;
      • Last-Event-ID;
      • Content-Type:只限于三个值application/x-www-form-urlencoded、multipart/form-data、text/plain;
  • 非简单请求:顾名思义,不是简单请求的请求都是非简单请求;

浏览器对两种不同请求的处理是不一样的。

简单请求

对于简单请求,浏览器会直接发送CORS请求,与此同时在header中增加Origin字段。Origin字段(协议+域名+端口号)主要用来表明请求来自哪个源,服务器可以根据请求源来判断是否统一这次请求。

如果Origin指定的源,不在许可范围内,服务器会返回一个正常的HTTP回应。浏览器发现,这个回应的头信息没有包含Access-Control-Allow-Origin字段(详见下文),就知道出错了,从而抛出一个错误,被XMLHttpRequest的onerror回调函数捕获。注意,这种错误无法通过状态码识别,因为HTTP回应的状态码有可能是200。

如果Origin在许可范围内,服务器会在响应的头部信息中增加几个字段:

Access-Control-Allow-Origin: http://api.bob.com
Access-Control-Allow-Credentials: true
Access-Control-Expose-Headers: FooBar
  • Access-Control-Allow-Origin:必须。它的值要么是请求头部中的Origin字段,要么是‘*’,表明支持所有的源;
  • Access-Control-Allow-Credentials:可选。表示是否允许发送Cookie,默认情况下Cookie不在CORS请求中,若设置为true,表明服务器允许Cookie包含在请求中发送给服务端。只能设置为true,若服务端不支持,删掉该字段即可。
  • Access-Control-Expose-Headers:可选。CORS请求时,XMLHttpRequest对象的getResponseHeader()方法只能拿到6个基本字段:Cache-Control、Content-Language、Content-Type、Expires、Last-Modified、Pragma。如果想拿到其他字段,就必须在Access-Control-Expose-Headers里面指定。上面的例子指定,getResponseHeader('FooBar')可以返回FooBar字段的值。

需要注意的是上面说到的请求发送Cookie的属性,不仅需要服务端在响应头中增加Access-Control-Allow-Credentials属性,还需要在发送请求时将xhr的withCredentials设置为true。

var xhr = new XMLHttpRequest();
xhr.withCredentials = true;

否则即使服务端同意发送Cookie,浏览器也不会发送。

如果要发送Cookie,服务端返回的Access-Control-Allow-Origin字段就不能为*了,必须是与请求方一致的域名。同时Cookie依然遵循同源策略,只有服务端设置的域名才能上传Cookie。

非简单请求

非简单请求就是对服务端有特殊要求的请求,对于非简单请求的CORS请求,浏览器会在发送正式请求之前增加一次http查询请求,俗称预检请求(preflight)。

浏览器先询问服务器,当前网页所在的域名是否在服务器的许可名单之中,以及可以使用哪些HTTP动词和头信息字段。只有得到肯定答复,浏览器才会发出正式的XMLHttpRequest请求,否则就报错。

举个栗子:

var url = 'http://api.alice.com/cors';
var xhr = new XMLHttpRequest();
xhr.open('PUT', url, true);
xhr.setRequestHeader('X-Custom-Header', 'value');
xhr.send();

如代码中所示,http发送一个PUT请求,同时发送一个自定义头字段X-Custom-Header

浏览器发现该请求为一个非简单请求,自动发送一个预检请求询问服务器是否允许这样的请求。下面是一个预检请求的头部信息:

OPTIONS /cors HTTP/1.1
Origin: http://api.bob.com
Access-Control-Request-Method: PUT
Access-Control-Request-Headers: X-Custom-Header
Host: api.alice.com
Accept-Language: en-US
Connection: keep-alive
User-Agent: Mozilla/5.0...

预检请求是一个OPTIONS请求,表示请求是用来询问的,除了必须的Origin字段,预检请求还包含几个特殊字段:

  • Access-Control-Request-Method:必须。用来列出浏览器CORS请求会用到那些HTTP方法;
  • Access-Control-Request-Headers:可选。用逗号分隔的字符串,用于表明浏览器CORS请求会在头信息中发送哪些额外的字段。

服务器在收到预检请求后,根据以上3个字段判断是否允许跨域请求,并做出回应:

HTTP/1.1 200 OK
Date: Mon, 01 Dec 2008 01:15:39 GMT
Server: Apache/2.0.61 (Unix)
Access-Control-Allow-Origin: http://api.bob.com
Access-Control-Allow-Methods: GET, POST, PUT
Access-Control-Allow-Headers: X-Custom-Header
Content-Type: text/html; charset=utf-8
Content-Encoding: gzip
Content-Length: 0
Keep-Alive: timeout=2, max=100
Connection: Keep-Alive

其中包含几个CORS相关的字段:

  • Access-Control-Allow-Origin:同简单请求;
  • Access-Control-Allow-Methods: 必须。用逗号分隔的字符串,用于表示服务器支持的HTTP方法,而不只是请求发送的方法,避免多次预检请求;
  • Access-Control-Allow-Headers:如果请求中带有次字段则服务器返回必须带上次字段,用于表明服务器支持的所有额外头信息字段,不只是请求发送的字段,避免多次预检请求;
  • Access-Control-Allow-Credentials:同简单请求;
  • Access-Control-Max-Age:可选。指定本次预检请求的有效期,有效期内不用重复发送预检请求。

如果浏览器否定了"预检"请求,会返回一个正常的HTTP回应,但是没有任何CORS相关的头信息字段。这时,浏览器就会认定,服务器不同意预检请求,因此触发一个错误,被XMLHttpRequest对象的onerror回调函数捕获。控制台会打印出如下的报错信息。

XMLHttpRequest cannot load http://api.alice.com.
Origin http://api.bob.com is not allowed by Access-Control-Allow-Origin.

一旦服务器通过了"预检"请求,以后每次浏览器正常的CORS请求,就都跟简单请求一样,会有一个Origin头信息字段。服务器的回应,也都会有一个Access-Control-Allow-Origin头信息字段。

PS:当需要带上cookie信息的时候,Access-Control-Allow-Origin就不能设置为"*"了;

CORS请求问题汇总总结的非常详细

服务器代理

浏览器有跨域限制,但是服务器不存在跨域问题,所以可以由服务器请求所要域的资源再返回给客户端。服务器代理是万能的。

参考文档