⤴Top⤴

CORS

博客分类: 前端

CORS

CORS

什么是同源策略

同源策略(same-origin policy),即域名协议端口必须都相同才是同源。不同源的客户端脚本在没明确授权的情况下,不能读写对方的资源。其目的是为了保证用户信息的安全,防止窃取数据。

以 http://a.example.com/blog/index.html 为例:

URL 结果 原因
http://a.example.com/blog2/other.html 成功 同源
http://a.example.com/blog/tate/another.html 成功 同源
https://a.example.com/secure.html 失败 不同协议(https 和 http)
http://a.example.com:8080/blog/etc.html 失败 不同端口(8080 和 80)
http://b.example.com/blog/other.html 失败 不同域名(b 和 a)

跨域解决方案

图像 Ping

图像 Ping 是使用 <img> 标签用于客户端与服务器间的单向通信。最常用于跟踪用户点击页面或动态广告曝光次数,有两个主要的缺点:

var img = new Image();
img.onload = img.onerror = function() { alert('tate'); };
img.src = 'http://a.example.com/test?name=snow' // 发送 name 参数

JSONP

JSONP(JSON with padding) 是应用 JSON 的一种新方法,只是被包含在函数调用中,如:

callback({'name': 'Tate'});

JSONP 由回调函数和数据组成,是通过动态 <script> 标签实现的,可以为 src 属性指定一个跨域 URL。

function handleResponse(res) {
  alert('you are at IP address' + res.ip + ',which is in' + res.city + ',' + res.region_name);
}

var script = document.createElement('script');
script.src = 'http://freegeoip.net/json/?callback=handleResponse';
document.body.insertBefore(script, document.body.firstChild);

iframe 形式

通常用于跨域操作 DOM 或简单的数据通信。

document.domain

document.domain 只适合主域相同但子域不同的情况,比如 a.example.com 和 b.example.com。解决方案是采用相同的主域:

document.domain = 'example.com';

如一个页面嵌套另一个页面,从而进行窗体间的交互,页面 a.html:

<!-- 地址 http://a.example.com/sop/a.html -->
<body>
  <iframe id="myFrame" src="http://b.example.com/sop/b.html"></iframe>
  <script>
    document.domain = 'example.com';
    $("iframe").load(function(){
      $(this).contents().find("h1").text('Tate');
    });
  </script>
</body>

页面 b.html:

<!-- 地址 http://b.example.com/sop/b.html -->
<!-- 两个页面同时设置主域时,便可将 h1 标签内容修改为 'Tate' -->
<body>
  <hi>Snow</hi>
  <script>
    document.domain = 'example.com';
  </script>
</body>

location.hash

location.hash 通过改变 hash 值来进行数据传递,不会造成页面刷新,此方法也叫片段标识符(fragment identifier),和锚点效果类似。如 http://a.example.com#tate 中的 ‘#tate’ 就是 location.hash。

如父窗口 a 可以把信息,写入子窗口的片段标识符:

var src = originURL + '#' + data;
document.getElementById('myFrame').src = src;

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

window.onhashchange = checkMessage;

function checkMessage() {
  var message = window.location.hash;
  // ...
}

然而大部分浏览器不允许子窗口 b 修改不同域的父窗口 a 的 hash 值(parent.location.hash),通常解决方案是可以在 b 页面中增加一个和 a 同域的 iframe(c.html)来做代理,这样 b 可以修改 c ,而 c 可以修改 a。

// b.html 页面
try {  //有的浏览器(如 Firefox)还是可以直接操作parent.location.hash的
  parent.location.hash = 'name=tate';
} catch (e) {
  // ie、chrome 的安全机制无法修改 parent.location.hash
  // 所以要利用一个代理 iframe,即 c.html
  var iframeProxy = document.createElement('iframe');
  iframeProxy.style.display = 'none';
  iframeProxy.src = 'http://a.example.com/sop/c.html#name=tate'; //必须跟a.html同域
  document.body.appendChild(iframeProxy);
}

window.name

window.name 值在不同的页面(即使不同域)加载后依旧存在,并且可以支持非常长的字符串,缺点是必须监听子窗口 window.name 属性的变化,影响网页性能。

通常解决方案为: 在 a 页面需要和不同域的 b 页面通信,我们可以现在 a 嵌入 b,待 b 有数据要传递时,把数据附加到 b 窗口的 window.name 上,然后把窗口跳转到一个和 a 同域的 c 页面,这样 a 就能轻松获取到内嵌窗体 c 的 window.name 了。同时 a 通过 setInterval 定时器的形式来达到轮询的效果。

以上示例细节均可以在 vajoy 博客 中查看。

跨文档消息传送

跨文档消息传送(cross-document messaging),简称 XDM,指在来自不同域的页面件传递信息,可以解决上面 iframe 跨域的”暴力破解”。是 HTML5 规范定义的 API,核心是 postMessage() 方法。该方法支持两个参数: 一条消息(通常为字符串)和一个表示目标域的字符串。

var iframeWindow = document.getElementById('myFrame').contentWindow;
iframeWindow.postMessage('Are you fine?', 'http://b.example.com');

接收到 XDM 消息后,会触发 message 事件,传递给 onmessage 处理程序的事件对象包含以下三个重要信息:

window.addEventListener('message', receiveMessage);

function receiveMessage(event) {
  if (event.origin !== 'http://a.example.com') return;
  if (event.data === 'Are you fine?') {
    event.source.postMessage('i am fine', event.origin);
  } else {
    console.log(event.data);
  }
}

WebSocket

WebSocket 是一种通信协议,使用 ws://(非加密)wss://(加密) 作为协议前缀。该协议不实行同源政策,只要服务器支持,就可以通过它进行跨源通信,HTTP 协议的通信只能由客户端发起,而 WebSocket 是实现全双工(full-duplex)、双向通信的,属于服务器推送技术的一种。

WebSocket

// 实例化 WebSocket 对象后,客户端便会与服务器进行连接
var ws = new WebSocket("wss://echo.websocket.org");

ws.onopen = function(evt) { // 用于指定连接成功后的回调函数
  console.log("Connection open ...");
  ws.send("Hello WebSockets!"); // 发送数据
};

ws.onmessage = function(evt) { // 用于指定收到服务器数据后的回调函数
  console.log( "Received Message: " + evt.data);
  ws.close(); // 关闭连接
};

ws.onclose = function(evt) { // 用于指定连接关闭后的回调函数
  console.log("Connection closed.");
};

与 XHR 类似,WebSocket 也有表示当前状态的 readyState 属性,但没有 readyStatechange 事件:

CORS

跨源资源分享(Cross-Origin Resource Sharing),简称 CORS。属于 W3C 标准,是 AJAX 跨域请求的根本解决方法。一般浏览器都通过 XHR(XMLHttpRequest) 对象实现了对 CORS 的原生支持,IE8 则引入了 XDR(XDomainRequest) 对象,其安全机制部分实现了 CORS 规范。

CORS

简单请求

某些请求不会触发 CORS 预检请求,则称为 简单请求,需要同时满足以下两个条件:

对于简单请求,浏览器直接发出 CORS 请求。即在头信息之中,增加一个 Origin 字段:

<!-- Origin 包含: 协议 + 域名 + 端口 -->

GET /cors HTTP/1.1
Origin: http://b.example.com
Host: a.example.com
Accept-Language: en-US
Connection: keep-alive
User-Agent: Mozilla/5.0...

若 Origin 指定的域名在许可范围内,服务器返回的响应,会多出几个头信息字段,若验证失败,则返回 403 状态码:

<!-- 值可以设为 *,表示接受任意域名的请求 -->
Access-Control-Allow-Origin: http://b.example.com

<!-- 可选,表示允许发送 cookie,只能设为 true,除非删除此字段,默认 cookie 不包括在 CORS 请求之中 -->
Access-Control-Allow-Credentials: true

<!-- 可选,CORS 请求时,XHR 对象的 getResponseHeader() 方法只能拿到 6 个基本字段:Cache-Control、Content-Language、Content-Type、Expires、Last-Modified、Pragma。 -->
<!-- 如果想拿到其他字段,就必须在 Access-Control-Expose-Headers 里面指定。如 getResponseHeader('FooBar') 可以返回 FooBar 字段的值。 -->
Access-Control-Expose-Headers: FooBar
Content-Type: text/html; charset=utf-8

非简单请求

非简单请求必须首先使用 OPTIONS 方法发起一个预检请求(Preflighted Requests)到服务器,以获知服务器是否允许该实际请求。需满足以下任意一个条件:

除了 Origin 字段,预检请求的头信息包括两个特殊字段:

OPTIONS /cors HTTP/1.1
Origin: http://b.example.com

<!-- 必须,用来列出浏览器的 CORS 请求会用到哪些 HTTP 方法 -->
Access-Control-Request-Method: PUT

<!-- 该字段是一个逗号分隔的字符串,指定浏览器 CORS 请求会额外发送的头信息字段 -->
Access-Control-Request-Headers: X-Custom-Header

Host: a.example.com
Accept-Language: en-US
Connection: keep-alive
User-Agent: Mozilla/5.0...

预检请求检验这些字段确定允许跨域请求后,作出如下回应,才会发出真正的请求,否则返回 403 状态码:

HTTP/1.1 200 OK
Date: Wed, 28 Feb 2018 12:35:39 GMT
Server: Apache/2.0.61 (Unix)

Access-Control-Allow-Origin: http://b.example.com
Access-Control-Allow-Methods: GET, POST, PUT
Access-Control-Allow-Headers: X-Custom-Header
Access-Control-Allow-Credentials: true

<!-- 可选,用来指定本次预检请求的有效期,单位为秒,在有效期间内不会再发送另一条预检请求 -->
Access-Control-Max-Age: 1800

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

withCredentials

默认情况下,cookie 不包括在 CORS 请求之中。若要发送 cookie,则服务端要设置:

Access-Control-Allow-Credentials: true

且一般情况下,客户端在 AJAX 请求中,也要设置 withCredentials 属性:

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

另外如果要发送 cookie,Access-Control-Allow-Origin 就不能设为星号,必须指定明确的、与请求网页一致的域名。同时,cookie 依然遵循同源政策,只有用服务器域名设置的 cookie 才会上传。

参考链接

  1. 浏览器同源政策及其规避方法 By 阮一峰
  2. 跨域资源共享 CORS 详解 By 阮一峰
  3. WebSocket 教程 By 阮一峰
  4. 浅谈 WEB 跨域的实现(前端向) By VaJoy Larn
  5. XMLHttpRequest 对象对 HTTP 请求的访问控制 By itbilu
  6. W3C - Cross-Origin Resource Sharing
  7. MDN - HTTP 访问控制(CORS)
  8. Codecademy - What is CORS ?
  9. Using CORS By Monsur Hossain
  10. RFC2616 - Header Field Definitions

Gitalking ...