同源策略(Same-origin policy)约束了两个域之间资源的加载方式,是一个很重要的安全机制用来隔离那些有潜在安全隐患的文档。javascript
一个源由一个URL的协议(protocol)、主机(host)和端口(port)进行定义。若是两个页面拥有相同的协议、主机和端口一致的话,咱们就能够称它们为同源。下面的表格比较了不一样的URL跟http://store.company.com/dir/page.html
这个URL的同源状况:html
URL | 是否同源 | 理由 |
---|---|---|
http://store.company.com/dir2... | 是 | |
http://store.company.com/dir/... | 是 | |
https://store.company.com/sec... | 否 | 协议不一样 |
http://store.company.com:81/d... | 否 | 端口不一样 |
http://news.company.com/dir/o... | 否 | 主机不一样 |
同源策略控制了两个源之间的交互,例如你使用XMLHttpRequest
发起一个请求,或者使用<img>
元素加载一张图片,就会产生两个源之间的交互。而当两个源不相同时,有些交互会被容许,而有些则不被容许。而不容许的状况,就是咱们常说的跨域访问问题。那什么状况下不一样源的交互会被容许,什么状况下又不被容许呢?大体能够分为以下的状况:java
连接、跳转和表单提交这些在跨域的状况下都是被容许的。例如调用支付宝接口进行支付就是典型的跨域表单提交的场景,这种跨域的调用是被容许的。可是这个很容易被利用来进行CSRF
攻击,因此咱们的表单提交须要作好这方面的防御。ajax
跨域的资源内嵌是被容许的。下面是一些资源内容的例子:json
使用<script src="..."></script>
加载Javascript。只有同源的脚本在语法错误时会显示错误信息。后端
使用<link rel="stylesheet" href="...">
加载CSS。跨源的CSS文件要求使用正确的Content-Type
响应头。api
使用<img>
加载图片。跨域
使用<video>
和<audio>
加载媒体文件。浏览器
使用 <object>
、<embed>
和<applet>
加载插件。安全
使用 @font-face
加载字体。有些浏览器容许加载跨域的字体,有些则不容许。
使用 <frame>
和<iframe>
加载任何东西。
跨域文档间使用Javascript脚本进行交互,API的访问有限制。例如使用ifame嵌入的页面或者使用window.open
打开的页面,若是跟父页面不一样源,则想经过Javascript去操做父页面的DOM,是不容许的(反过来亦然)。
举个例子,假设有这么一个页面http://www.example.com/index.html
:
<!DOCTYPE html> <html> <head> ..... </head> <body > <iframe src="http://sub.example.com/iframe.html" frameborder="0"></iframe> </body>
而后在http://sub.example.com/iframe.html
页面对父页面的背景色进行修改:
<!DOCTYPE html> <html> <head> ...... </head> <body > <script> window.parent.document.body.style.backgroundColor = 'green'; </script> </body>
因为两个页面不一样源,因此子页面对父页面的操做被禁止,例如在Firefox上你会看到如下的报错:
Error: Permission denied to access property "document"
不一样源之间的XMLHttpRequest
调用(也就是咱们常说ajax调用)是不被容许的,这个也是咱们最常遇到的跨域访问场景。例如咱们在http://example.com/index.html
页面进行以下的ajax调用:
var xhr = new XMLHttpRequest(); var url = 'http://otherexample.com/api/get-data'; xhr.open('GET', url, true); xhr.onreadystatechange = handler; xhr.send();
在Firefox下会报以下错误:
已拦截跨源请求:同源策略禁止读取位于 http://otherexample.com/api/get-data 的远程资源。(缘由:CORS 头缺乏 'Access-Control-Allow-Origin')。
一个页面的源是能够修改的,修改的方法很简单,就是经过Javascript脚本设置document.domain
的值。举个例子,咱们在页面http://store.company.com/dir/other.html
执行下面的代码:
document.domain = 'company.com';
那么这个页面的域将会由store.company.com
变成company.com
,后面在判断是否同源的时候,主机将会使用company.com
这个值,而不是store.company.com
。
那是否是修改域后就能跟同域的页面进行交互呢。答案是否认的。例如,若是你在页面中经过iframe嵌入http://company.com/dir/page.html
这个页面,而后经过javascript去跟这个页面交互,你会发现浏览器会报错,就跟咱们以前那个例子同样。按照上面的源的定义,这时候两个页面应该是同源的,为何呢?由于修改document.domain
会致使端口号被设为null
。因此另一个页面也须要把document.domain
修改成相同的值,这样两个页面的主机和端口就一致了,能够进行互相的访问了。
既然源能够修改,那么是否是就解决了咱们的跨域问题呢?明显没这么简单。首先,这种方法是有很大限制条件的,document.domain
这个值只能修改成这个页面的当前域或者当前域的超级域。因此,这个方法只能解决同一超级域下的页面跨域问题。其次,它的使用场景也颇有限,由于它须要页面执行Javascript脚本,因此也就是说通常只能应用于页面跟页面的交互,例如访问ifame页面或者window.open
打开的页面等等。因此若是你想用来解决ajax
之类的跨域调用,这个方法就无能为力了。
使用代理也是解决跨域访问的一个方法。上面修改document.domain
的方法只能用来访问子域名的页面,没法访问不一样域的页面,而使用代理则没有这个问题。
例如咱们有一个页面http://example.com/
,须要访问http://otherexample.com/
这个页面,咱们不直接对这个页面进行访问,而是经过请求另一个同源的页面,这个页面在后端经过代理服务器把请求转发到http://otherexample.com/
,获取数据并返回给客户端。
另外,这个方法一样能够用于解决ajax
的跨域访问问题。
JSONP
也被常常用来解决ajax
的跨域调用问题。JSONP
请求并非经过XMLHttpRequest
发起,而是使用<script>
进行调用。前面说过,内嵌资源通常不受同源政策影响,因此<script>
能够加载其余源的资源。
举个例子,假设http://www.example.com/
页面,想异步调用http://www.otherexample.com/ajax.json
这个接口,这个接口会返回以下的数据:
{ "id": "123", "name": "Captain Jack Sparrow" }
若是咱们经过XMLHttpRequest
发起调用,就会由于同源政策而失败。因此咱们经过<script>
进行调用,并经过参数传递咱们的回调函数名:
<script src="http://www.otherexample.com/ajax.json?callback=myFunction"></script>
而后接口获取到callback函数名后,把原来返回的数据做为函数的参数,最终返回以下的Javascript:
myFunction({"id": "123", "name": "Captain Jack Sparrow"});
而后myFunction
就会执行,达到了调用的目的。
这个方法在大多数状况下都颇有用,可是它也有它的局限。一是它须要后端的配合,由于后端的接口须要根据约定的参数获取回调函数名,而后跟返回数据进行拼接,最后进行响应。二是它只能进行异步的调用,由于它的原理是经过动态生成<script>
加载JS的方法,而这个过程是异步的,因此若是你想进行同步的调用,那么这个方法就无能为力了。
Web Messaging
(又称cross-document messaging
)是HTML5的一个接口,容许两个不一样源的文档之间进行通讯。
它主要用到了接口里的postMessage
方法,这个方法能够把纯文本消息从一个域发送到另一个域。消息能够发送如下的对象:
发送方文档里frame
和iframe
。
发送方经过Javascript打开的页面。
发送方的父页面。
打开发送方页面的页面。
消息event
包含了如下的属性:
data
:收到的消息。
origin
:发送方的源,包括协议、主机名和端口。
source
:发送方的window
对象。
举个例子,假设example.net
下的文档A跟文档里用iframe加载的example.com
下的文档B进行通讯,咱们向文档B发送消息Hello B
,Javascript代码大体以下:
var o = document.getElementsByTagName('iframe')[0]; o.contentWindow.postMessage('Hello B', 'http://example.com/');
咱们先获取到文档B的contentWindow
对象,而后把须要发送到消息以及文档B的源传给postMessage
。文档B则经过监听message
事件,捕获到事件,并做相应的处理:
function receiver(event) { if (event.origin == 'http://example.net') { if (event.data == 'Hello B') { event.source.postMessage('Hello A, how are you?', event.origin); } else { alert(event.data); } } } window.addEventListener('message', receiver, false);
须要注意的是,postMessage
是个非阻塞的调用,也就是说是异步的。
Web Messaging
主要用于跨域文档间的通信,因此它不能用来解决全部跨域调用的问题,例如ajax
调用。并且IE浏览器对它的支持也很有限。
CORS
(Cross-Origin Resource Sharing)是W3C提出的一个用于服务器端控制数据跨域传输的一个机制。 它的原理是经过一些新增长的HTTP头让服务端能定义哪些源的请求能够被容许。
简单举个例子,假设咱们在页面http://example.com/
发起一个跨域的XMLHttpRequest
请求:
var xhr = new XMLHttpRequest(); var url = 'http://otherexample.com/api/get-data/'; xhr.open('GET', url, true); xhr.onreadystatechange = handler; xhr.send();
正常状况这个请求是不容许的。可是若是咱们在服务端返回如下响应头:
Access-Control-Allow-Origin: *
这个Access-Control-Allow-Origin
头表示服务端容许哪些源的请求,*
表示容许全部的源,因此上面的请求就被容许了。固然正常状况下咱们不会这样作,咱们须要把Access-Control-Allow-Origin
设置为真正想容许的源。在请求头会有一个叫Origin
的头,它的值就是请求方的源(例如上面的请求会有Origin: http://example.com
这个请求头),服务端应该根据这个头去返回相应的Access-Control-Allow-Origin
头。
固然CORS
的实际使用会比上面的例子复杂得多,具体能够参考MDN的这篇文章和W3C的规范。
CORS
能够说是解决XMLHttpRequest
跨域调用的一个比较好的方法,但IE浏览器对它的支持一样很有限,直到IE11才彻底支持,因此在移动端更能发挥它的做用。