0%

同源策略及其解决方案

“同源政策”是浏览器安全的基石,其设计目的是为了保证信息安全,防止恶意的网站窃取数据。所谓“同源”必须满足以下三个方面:

  1. 协议相同
  2. 域名相同
  3. 端口相同(默认端口是80,可以省略)

如果是非同源的,以下行为会受到限制:

  • Cookie、LocalStorageIndexDB无法读取
  • DOM无法获取
  • AJAX请求不能发送

接下来我们主要讲解如何解决以上三个方面的问题。

一、Cookie

Cookie只有同源的网站才能获取,但是如果两个网页的一级域名相同,只是二级域名不同,可以设置相同的document.domain,两个网页就可以共享cookie了。

很多人都误把带www当成一级域名,把其他前缀的当成二级域名,是错误的。正确的域名划分为:
1.顶级域名:.com
2.一级域名:baidu.com
3.二级域名:tieba.baidu.com

举例来说,A网页是http://w1.sillywa.com/a.html,B网页是http://w2.sillywa.com/b.html,我们可以设置

1
document.domain = 'sillywa.com'

这样两个网页就可以共享Cookie了。

注意,这种方法只是用于CookieiframeLocalStorageIndexDB无法通过这种方法规避同源政策,而是要是用PostMessage API,下面我们会介绍。

二、iframe

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

所以对于完全不同源的网站,目前可以使用一下三种办法规避同源问题:

  • 片段标识符(fragment identifier)
  • window.name
  • 跨文档通信API(window.postMessage)

1.片段标识符

片段标识符指的是URL#后面的内容,比如http://sillywa.com/a.html#fragment中的#fragment,如果只是改变片段标识符,页面不会重新刷新。

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

1
2
var src = originURL + '#' + data
document.getElementById('myIframe').src = src

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

1
2
3
window.onhashchange = function() {
console.log(window.location.hash)
}

2.window.name

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

3. window.postMessage

HTML5为了解决跨窗口通讯问题引入了一个新的API:跨文档通信API。这个APIwindow新增了一个window.postMessage()方法,允许跨窗口通讯,不论这两个窗口是否同源。举例来说:假设父窗口为:http://aaa.com,子窗口为:http://bbb.com

1
2
3
// 父窗口向子窗口发送消息
var popup = window.open('http://bbb.com', 'title');
popup.postMessage('Hello World!', 'http://bbb.com');

postMessage()方法的第一个参数是具体的信息内容,第二个参数是接收消息的窗口的源(origin),即”协议 + 域名 + 端口”。也可以设为*,表示不限制域名,向所有窗口发送。

同样,子窗口向父窗口发送消息可以这样写:

1
window.opener.postMessage('Nice to see you', 'http://aaa.com');

父窗口和子窗口都可以通过message事件,监听对方的消息:

1
2
3
window.addEventListener('message', function(e) {
console.log(e.data)
},false)

message事件的event对象有以下三个属性:

  1. event.source:发送消息的窗口
  2. event.origin:消息发送的网址
  3. event.data:消息内容

下面的例子是,子窗口通过event.source属性引用父窗口,然后发送消息。

1
2
3
4
window.addEventListener('message', receiveMessage);
function receiveMessage(event) {
event.source.postMessage('Nice to see you!', '*');
}

如果我们将发送的消息改为LocalStorage,则可以互相读取LocalStorage

三、AJAX

同样AJAX请求也会受到同源策略的影响,除了使用代理服务器外,还有一下方法可以实现跨域:

  • jsonp
  • WebScoket
  • CORS

1.jsonp

jsonp想必大家都很了解,其由两部分组成:回调函数和数据。其基本思路是:动态插入script标签,向服务器请求json数据,返回的数据将在回调函数里获得。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
function addScriptTag(src) {
var script = document.createElement('script');
script.setAttribute("type","text/javascript");
script.src = src;
document.body.appendChild(script);
}
// 定义回调函数
function foo(data) {
console.log('Your public IP address is: ' + data.ip);
};

window.onload = function () {
addScriptTag('http://example.com/ip?callback=foo');
}

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

2.WebScoket

WebScoket不同于http,它提供一种双向通讯的功能,即客户端可以向服务器请求数据,同时服务器也可以向客户端发送数据。而http只能是单向的。

同时WebScoket使用ws:\//(非加密)和wss:\//(加密)作为协议前缀。该协议不实行同源政策,只要服务器支持,就可以通过它进行跨源通信。

要创建WebScoket,先实例化一个WebScoket对象并传入要连接的URL

1
var scoket = new WebScoket("ws://www.example.com/server.php")

实例化WebScoket对象之后,浏览器会马上尝试建立连接。与XHR类似,WebScoket也有一系列表示当前状态的readyState属性,如下:

  • WebScoket.OPENING (0):正在建立连接
  • WebScoket.OPEN (1):已经建立连接
  • WebScoket.CLOSING (2):正在关闭连接
  • WebScoket.ClOSE (3):已经关闭连接

WebScoket没有readyStatechange事件;不过它有其他的事件,我们待会介绍。

要关闭WebScoket连接,可以调用close()方法:

1
scoket.close()

WebScoket连接之后,就可以发送和就收数据。要发送数据可以调用send()方法,并传入字符串,例如:

1
2
var scoket = new WebScoket("ws://www.example.com/server.php")
scoket.send('hello word')

因为WebScoket只能发送纯文本数据,所以对于复杂的数据类型我们应先将其序列化转化为json字符串

1
2
3
4
var message = {
name: 'sillywa'
}
scoket.send(JSON.stringify(message))

同样服务器必须先解析再读取数据。

当服务器向客户端发来消息时,WebScoket对象就会触发message事件。这个message事件与其它传递消息的协议类似,也就是把返回的数据保存在event.data的属性中。

1
2
3
scoket.onmessage = function(event) {
console.log(event.data)
}

与通过send()发送到服务器的数据一样,event.data中返回的数据也是字符串。

WebScoket对象还有其他三个事件,在连接生命周期的不同阶段触发。

  • open:在成功建立连接时触发
  • error:在发生错误时触发,连接不能持续
  • close:在连接关闭时触发

WebScoket对象不支持DOM2级事件侦听器,因此必须使用DOM0级语法分别定义每个事件处理程序。

1
2
3
4
5
6
7
8
9
10
var scoket = new WebScoket("ws://www.example.com/server.php")
scoket.onopen = function() {
console.log('connection start')
}
scoket.onerror = function() {
console.log('connection error')
}
scoket.onclose = function(event) {
console.log(event)
}

在这三个事件中只有closeevent对象有额外的信息。这个事件的对象有三个额外的属性:wasClean、code、reason。其中wasClean是一个布尔值,表示连接是否已经明确地关闭;code是服务器返回的数值状态码;reason是一个字符串,包含服务器发回的信息。

3.CORS

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

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

相比jsonp只能发送get请求,CORS允许发送任何类型的请求。但CORS要求浏览器和服务器同时支持。目前所有浏览器都支持,IE需要IE10以上。

整个CORS通讯过程中都是浏览器自动完成,不需要用户的参与。CORS通讯和同源的AJAX请求没有区别。浏览器一旦发现AJAX请求跨域,就会自动添加一些头部信息,有时候还会多出一次附加请求。

浏览器将CORS请求分为两类:简单请求和非简单请求。

只要同时满足一下两个条件就是简单请求,否则就是非简单请求:

(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

对于简单请求,浏览器会自动在头部信息里增加一个Origin字段,用来表示请求来自与哪个源,服务器根据这个值决定是否同意此次请求。如果Origin不在请求范围内,服务器返回一个正常的http回应。这个回应的头信息中没有Access-Control-Allow-Origin字段,浏览器发现没有这个字段之后就会抛出一个错误。如果Origin在请求范围内,服务器返回的响应会多出几个头信息字段,其中一个是Access-Control-Allow-Origin,它的值要么是Origin的值,要么是*,表示允许任何域名的请求。

对于非简单请求,它会在正式通信之前,增加一次http查询请求,称为”预检”请求(preflight)。通常是一个OPTION请求。这个请求先询问服务器,当前网页所在的域名是否在服务器的许可名单之中,以及可以使用哪http动词和头信息字段。只有得到肯定答复,浏览器才会发出正式的XMLHttpRequest请求,否则就报错。

如果大家想要更详细的了解CORS,可以参考以下文章。

参考文章:

阮一峰《浏览器同源政策及其规避方法

阮一峰《跨域资源共享 CORS 详解

参考书籍:

《javascript高级程序设计》