同源策略及跨域访问
同源策略
同源策略(Same-origin policy)约束了两个域之间资源的加载方式,是一个很重要的安全机制用来隔离那些有潜在安全隐患的文档。
何为源(orgin)
一个源由一个URL的协议(protocol)、主机(host)和端口(port)进行定义。如果两个页面拥有相同的协议、主机和端口一致的话,我们就可以称它们为同源。下面的表格比较了不同的URL跟http://store.company.com/dir/page.html
这个URL的同源情况:
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>
元素加载一张图片,就会产生两个源之间的交互。而当两个源不相同时,有些交互会被允许,而有些则不被允许。而不允许的情况,就是我们常说的跨域访问问题。那什么情况下不同源的交互会被允许,什么情况下又不被允许呢?大致可以分为如下的情况:
链接、跳转和表单提交这些在跨域的情况下都是被允许的。例如调用支付宝接口进行支付就是典型的跨域表单提交的场景,这种跨域的调用是被允许的。但是这个很容易被利用来进行
CSRF
攻击,所以我们的表单提交需要做好这方面的防护。跨域的资源内嵌是被允许的。下面是一些资源内容的例子:
使用
<script src="..."></script>
加载Javascript。只有同源的脚本在语法错误时会显示错误信息。使用
<link rel="stylesheet" href="...">
加载CSS。跨源的CSS文件要求使用正确的Content-Type
响应头。使用
<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
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
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
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才完全支持,所以在移动端更能发挥它的作用。
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。