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> 标签用于客户端与服务器间的单向通信。最常用于跟踪用户点击页面或动态广告曝光次数,有两个主要的缺点:
- 只能发送 GET 请求
- 无法访问服务器的响应文本
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 处理程序的事件对象包含以下三个重要信息:
- data - 消息内容
- origin - 来源,发送消息的文档所在的域
- source - 目标,发送消息的文档的 window 对象代理
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 对象后,客户端便会与服务器进行连接
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 事件:
- WebSocket.CONNECTING - 值为 0,表示正在连接。
- WebSocket.OPEN - 值为 1,表示连接成功,可以通信了。
- WebSocket.CLOSING - 值为 2,表示连接正在关闭。
- WebSocket.CLOSED - 值为 3,表示连接已经关闭,或者打开连接失败。
CORS
跨源资源分享(Cross-Origin Resource Sharing),简称 CORS。属于 W3C 标准,是 AJAX 跨域请求的根本解决方法。一般浏览器都通过 XHR(XMLHttpRequest) 对象实现了对 CORS 的原生支持,IE8 则引入了 XDR(XDomainRequest) 对象,其安全机制部分实现了 CORS 规范。
简单请求
某些请求不会触发 CORS 预检请求,则称为 简单请求,需要同时满足以下两个条件:
- 请求方法是以下三种方法之一:
- HEAD
- GET
- POST
- 不得人为设置对 CORS 安全的首部字段集合之外的其他首部字段,如:
- Accept
- Accept-Language
- Content-Language
- Content-Type:仅限于三个值:
- text/plain
- multipart/form-data
- application/x-www-form-urlencoded
对于简单请求,浏览器直接发出 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)到服务器,以获知服务器是否允许该实际请求。需满足以下任意一个条件:
- 请求方法不是以下方法之一(可以是 PUT 或 DELETE 等):
- HEAD
- GET
- POST
- 或者人为设置了对 CORS 安全的首部字段集合之外的其他首部字段,其中 Content-Type 不包含于以下三个值(可以是 application/json 等):
- text/plain
- multipart/form-data
- application/x-www-form-urlencoded
除了 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 才会上传。
参考链接
- 浏览器同源政策及其规避方法 By 阮一峰
- 跨域资源共享 CORS 详解 By 阮一峰
- WebSocket 教程 By 阮一峰
- 浅谈 WEB 跨域的实现(前端向) By VaJoy Larn
- XMLHttpRequest 对象对 HTTP 请求的访问控制 By itbilu
- W3C - Cross-Origin Resource Sharing
- MDN - HTTP 访问控制(CORS)
- Codecademy - What is CORS ?
- Using CORS By Monsur Hossain
- RFC2616 - Header Field Definitions
Gitalking ...