跨域资源共享 CORS

CORS 是一个 W3C 标准, 全称是”跨域资源共享”(cross-origin resource sharing)

他允许浏览器向跨域服务器发出 XMLHttpRequest, 从而克服了 AJAX 只能同源使用的限制

简介

CORS 需要浏览器和服务器同时支持. 目前所有浏览器都支持该功能, IE 浏览器不能低于 IE10.

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

因此, 实现 CORS 的关键是服务器, 只要服务器实现了 CORS 接口, 就可以跨域通信

两种请求

CORS 请求分两类: 简单请求(simple request)和非简单请求(not-so-simple request)

只要同时满足以下两大条件, 就属于简单请求:

  1. 请求方法是: HEAD, GET 或者 POST
  2. HTTP 头信息不超出以下几种字段: Accept, Accept-Language, Content-Language, Last-Event-ID, Content-Type(只限于三个值: application/x-www-form-urlencoded, multipart/form-data, text/plain)

凡是不能同时满足上面两个条件的, 就属于非简单请求.

简单请求

基本流程

对于简单请求, 浏览器直接发出 CORS 请求, 具体来说, 就是在头信息中增加一个Origin字段

GET /cors HTTP/1.1
Origin: http://api.bob.com
Host: api.alice.com
Accept-Language: en-US
Connection: keep-alive
User-Agent: Mozilla/5.0...
`</pre>

上面的头信息中, `Origin`字段用来说明, 本次请求来此哪个源(协议+域名+端口), 服务器根据这个值, 决定是否同意此次请求.

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

如果`Origin`指定的域名在许可范围内, 服务器返回的相应, 会多出几个头信息字段

<pre>`Access-Control-Allow-Origin: http://api.bob.com
Access-Control-Allow-Credentials: true
Access-Control-Expose-Headers: FooBar
Content-Type: text/html; charset=utf-8
`</pre>

上面的头信息中, 有三个与 CORS 请求相关的字段, 都以 `Access-Control-` 开头

##### Access-Control-Allow-Origin

这个字段是必须的, 他的值要么是请求时 `Origin` 的值, 要么是一个 `*`, 表示接收任意域名的请求

##### Access-Control-Allow-Credentials

该字段是可选的, 是一个布尔值, 表示是否允许发送 Cookie. 默认情况下, Cookie 不包括在 CORS 请求之中.

设为 `true` 表示服务器明确许可, Cookie 可以包括在请求之中, 一起发给服务器

这个值也只能设为`true`, 如果服务器不需要浏览器发送 Cookie, 删除字段即可

##### 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` 字段的值

#### withCredentials 属性

CORS 请求默认不发送 Cookie 和 HTTP 认证信息, 如果要把 Cookie 发送给服务器, 一方面要服务器同意, 指定 `Access-Control-Allow-Credentials` 字段

<pre>`Access-Control-Allow-Credientials: true
`</pre>

另一方面, 开发者需要在 AJAX 请求中打开`withCredientials`属性

<pre>`var xhr = new XMLHttpRequest();
xhr.withCredientials = true;
`</pre>

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

要注意, 如果要发送 Cookie, `Access-Control-Allow-Origin` 不能设置为 `*`, 必须指定明确的, 与请求网页一致的域名. 同时 Cookie 依然遵循同源政策, 只有用服务器域名设置的 Cookie 才会上传, 其他域名的 Cookie 不会上传.

### 非简单请求

#### 预检请求

非简单请求是那种对服务器有特殊要求的请求, 比如请求方法是 `PUT`, 或 `DELETE`, 或者 `Content-Type` 字段类型是 `application/json`

非简单请求的 CORS 请求, 会在正是通信钱, 增加一次 HTTP 查询请求, 称为 "预检请求" (preflight)

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

下面是一段浏览器的 JavaScript 脚本

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

上面代码中, HTTP 请求的方法是 `PUT`, 并且发送一个自定义头信息 `X-Custom-Header`

浏览器发现这是一个非简单请求, 就自动发出一个 "预检" 请求, 要求服务器确认可以这样请求. 下面是这个 "预检" 请求的 HTTP 头信息

<pre>`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
Connection: keep-alive
User-Agent: Molliza/5.0...
`</pre>

"预检" 请求用的请求方法是 `OPTIONS`, 表示这个请求是用来询问的, 头信息里面, 关键字段是 `Origin`, "预检" 秦秋的头信息包括两个特殊字段
- Access-Control-Request-Method
  这个字段是必须的, 用来列出请求方法
  • Access-Control-Request-Headers
    该字段是一个逗号分隔的字符串, 永安里指定浏览器 CORS 请求会格外发送的头信息字段

    预检请求的响应

    服务器取得”预检”请求后, 检查Origin, Access-Control-Request-MethodAccess-Control-Request-Header 子弹后, 确认允许跨源请求, 就可以做出回应

    `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
    Content-Type: text/plain
    `

    上面 HTTP 响应中, 关键的是 Access-Control-Allow-Origin 字段, 表示 http://api.bob.com 可以请求数据, 该字段也可以设置为*, 表示同意跨源请求.

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

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

    服务器回应其他 CORES 相关字段如下:

    `Access-Control-Allow-Methods: GET, POST, PUT
    Access-Control-Allow-Headers: X-Custom-Header
    Access-Control-Allow-Credentials: true
    Access-Control-Max-Age: 1728000
    `
    Access-Control-Allow-Method

    该字段是必须的, 他的值是逗号分隔的一个字符串, 表明服务器支持的跨域请求方式

    Access-Control-Allow-Headers

    如果浏览器包含Access-Control-Request-Headers, 则 Access-Control-Allow-Headers 也是必须的, 其值也是一个逗号分隔的字符串

    Access-Control-Allow-Credentials

    该字段与简单请求时的含义相同

    Access-Control-Max-Age

    该字段可选, 永安里指定本次预检请求的有效期(s), 有效期内发起跨域请求不需要预检请求.

    浏览器的正常请求与回应

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

    “预检”请求之后, 浏览器的正常 CORS 请求

    `PUT /cors HTTP/1.1
    Origin: http://api.bob.com
    Host: api.alice.com
    X-Custom-Header: value
    Access-Language: en-US
    Connection: keep-alive
    User-Agent: Mozilla/5.0...
    `

    其中Origin字段是浏览器自动添加的

    服务器的正常回应

    `Access-Control-Allow-Origin: http://api.bob.com
    Content-Type: text/html; charset=utf-8

浏览器同源政策及其规避方法

浏览器安全的基石是”同源政策”(same-origin policy).

概述

所谓”同源”, 是指三个”相同”.

  • 协议相同: http:\\
  • 域名相同: www.example.comexample.com 不同
  • 端口相同

目的

同源政策的目的是为了保证用户信息安全, 防止恶意的网络窃取数据

限制范围

如果非同源, 共有三种行为受到限制

  • Cookie, LocalStorage 和 IndexDB 无法读取
  • DOM 无法获得
  • AJAX 请求无法发送

Cookie 是服务器写入浏览器的一小段信息, 只有同源的网站才能共享. 但是两个网页一级域名相同, 只有二级域名不同, 浏览器允许通过设置 document.domain 共享 Cookie.

举例来说, A 网页是 http://w1.example.com/a.html, B 网页是 http://w2.example.com/b.html, 那么只要设置相同的 document.domain, 这两个网页可以共享 Cookie.

document.domain = 'example.com';
`</pre>

现在 A 网页通过脚本设置一个 Cookie

<pre>`document.cookie = 'test1=hello';
`</pre>

B 网页就可以读取这个 Cookie

<pre>`var allCookie = document.cookie;
`</pre>

注意, 这种方法只适用于 Cookie 和 iframe 窗口, LocalStorage 和 IndexDB 无法通过这种方法, 规避同源政策.

另外, 服务器也可以在设置 Cookie 的时候指定 Cookie 所属域名为一级域名, 比如 `.example.com`

<pre>`Set-Cookie: key=value; domain=.example.com; path=/
`</pre>

这样的话, 二级域名和三级域名不用做任何设置就可以读取这个 Cookie

### iframe

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

如果两个窗口一级域名相同, 只是二级域名不同, 那么设置`document.domain`也可以规避同源政策, 实现 DOM 获取.

### 完全不同源的网站

#### 片段识别符(fragment identifier)

片段识别符值得是 URL 中`#` 后的部分, 也就是 hash.

如果只是改变片段识别符, 页面不会重新刷新

父窗口可以把信息写入子窗口的片段识别符

<pre>`var src = originURL + '#' + data;
document.getElementById('myFrame').src = src;
`</pre>

子窗口通过监听 `hashchange` 事件得到通知

<pre>`window.onhashchange = checkMessage;

function checkMessage(){
  var message = window.location.hash;
  // ...
}
`</pre>

同样子窗口也可以改变父窗口的片段识别符

<pre>`parent.location.href = target + "#" + hash;
`</pre>

#### window.name

浏览器窗口有 `window.name` 属性, 这个属性的最大特点是, 无论是否同源, 只要在同一个窗口里, 前一个网页设置这个属性后, 后一个网页可以读取他

父窗口先打开一个子窗口, 载入一个不同源的网站, 该网页将写入 `widnow.name` 属性

<pre>`window.name = data;
`</pre>

接着, 子窗口跳回一个与主窗口同域的网址

<pre>`location = 'http:parent.url.com/xxx.html'
`</pre>

然后主窗口就可以读取子窗口的 `window.name` 属性了

<pre>`var data = document.getElementById('myFrame').contentWindow.name;
`</pre>

这种方法的优点是 `window.name` 容量大, 缺点是必须监听子窗口的 `window.name` 属性的变化, 影响性能.

#### window.postMessage

以上信息都属于破解, HTML5 为了解决这个问题, 引入一个全新的 API: 跨文档通信 API(Cross-document messaging)

这个 API 为 `window` 对象新增一个 `window.postMessage` 方法, 允许跨窗口通信, 不论两个窗口是否同源.

举例来说, 父窗口 `http://aaa.com` 向子窗口 `http://bbb.com` 发消息, 调用 `postMessage` 方法就可以了

<pre>`var popup = window.open('http://bbb.com', 'title');
popup.postMessage('Hello World', 'http://bbb.com');
`</pre>

`postMessage` 方法的第一个参数是要传递的 Message, 第二个参数是接受信息的窗口的源(origin), 即`协议+域名+端口`, 也可以设置为`*`, 表示不限制域名, 向所有窗口发送.

子窗口向父窗口发送消息的写法类似

<pre>`window.opener.postMessage('Hello World', 'http://aaa.com');
`</pre>

通过`message`事件监听对象消息

<pre>`window.addEventListener('message', function(e){
console.log(e.data);
}, false);
`</pre>

`message` 事件的事件对象 `event` , 提供一下三个属性
  • event.source: 发送信息的窗口, 子窗口通过 event.source 引用父窗口然后发送消息
    `window.addEventListener('message', receiveMessage);
    function receiveMessage(event){
    event.source.postMessage('Hello Opener', '*');
    }
    `
  • ‘event.origin’: 发送信息的网址(‘http://aaa.com'), 可以过滤不是指定来源的信息
    `window.addEventListener('message', receiveMessage);
    function receiveMessage(event){
    if (event.origin != 'http://aaa.com') return;
    if (event.data === 'Hello World') {
      event.source.postMessage('Hello', event.origin);
    } else {
      console.log(event.data);
    }
    }
    `
  • ‘event.data’: 信息内容

    LocalStorage

    通过 window.postMessage 读写其他窗口的 LocalStorage 也是可能的

    例: 主窗口写入 iframe 子窗口的 localStorage

    `window.onmessage = function(event){
      if(event.origin !== 'http://bbb.com') return;
      var payload = JSON.parse(event.data);
      localStorage.setItem(payload.key, JSON.stringify(payload.data));
    };
    `

    上面代码中, 子窗口将父窗口发送的消息写入自己的 localStorage

    父窗口发送消息的代码如下:

    `var win = document.getElementByTagName('iframe')[0].contentWindow;
    var obj = { name: 'Jack' };
    win.postMessage(JSON.stringify({key: 'storage', data: obj}), 'http://bbb.com');
    `

    加强版子窗口接收消息的代码

    `window.onmessage = function(e) {
      if (e.origin !== 'http://bbb.com') return;
      var payload = JSON.parse(e.data);
      switch(payload.method) {
        case 'set': localStorage.setItem(payload.key, JSON.stringify(parload.data));
                    break;
        case 'get': var parent = window.parent;
                    var data = localStorage.getItem(payload.key);
                    parent.postMessage(data, 'http://aaa.com');
                    break;
        case 'remove': localStorage.removeItem(payload.key);
                       break;
      }
    };
    `

    加强版的父窗口消息发送代码

    `var win = document.getElementByTagName('iframe')[0].contentWindow;
    var obj = { name: 'Jack' };
    // 存入对象
    win.postMessage(JSON.stringify(key: 'storage', method: 'set', data: obj), 'http://bbb.com');
    // 读取对象
    win.postMessage(JSON.stringify(key: 'storage', method: 'get',), '*');
    window.onmessage = function(e){
      if(e.origin !== 'http://bbb.com') return;
      // 'Jack'
      console.log(JSON.parse(e.data).name);
    };
    `

    AJAX

    同源政策规定, AJAX 请求只能发给同源的网址, 否则会报错

    除了假设服务器代理, 有三中方法规避这个限制

    JSONP

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

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

    首先, 网页动态插入&lt;script&gt;元素, 由它向跨源网址发出请求.

    `function addScriptTag(src) {
      var script = document.createElement('script');
      script.setAttribute('type', 'text/javascript');
      script.src = src;
      document.body.appendChild(script);
    }
    window.onload = function(){
      addScritpTag('http://example.com/ip?callback=foo');
    }
    
    function foo(data){
      console.log("Your public IP address is: " + data.ip);
    };
    `

    上面的代码通过动态添加&lt;script&gt;元素, 向服务器example.com发出请求. 注意, 该请求的查询字符串有一个callback 参数, 用来指定回调函数的名字, 这对 JSONP 是必须的.

    服务器接收到这个请求后, 会将数据放在回调函数的参数位置返回.

    `foo({
      'ip': '8.8.8.8'
    });
    `

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

    WebSocket

    WebSocket 是一种通信协议, 使用ws://(非加密)和wss://(加密)作为协议前缀.

    该协议不实行同源政策, 只要服务器支持, 就可以与他进行夸源通信.

    下面是一个例子, 浏览器发出的 WebSocket 请求的头信息:

    `GET /chat HTTP/1.1
    Host: server.example.com
    Upgrade: websocket
    Connection: Upgrade
    Sec-WebSocket-Key: x3JJHMbDL1EzLkh9GBhXDw==
    Sec-WebSocket-Protocol: chat, superchat
    Sec-WebSocket-Version: 13
    Origin: http://example.com

上面代码中, 有一个字段是Origin, 表示该请求的请求源(origin)

正式因为有了Origin这个字段, 所以 WebSocket 才没有实行同源政策, 因为服务器可以根据这个字段判断是否许可此次通信

CORS

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

Your browser is out-of-date!

Update your browser to view this website correctly. Update my browser now

×