“同源政策”是浏览器安全的基石,其设计目的是为了保证信息安全,防止恶意的网站窃取数据。所谓“同源”必须满足以下三个方面:
- 协议相同
- 域名相同
- 端口相同(默认端口是80,可以省略)
如果是非同源的,以下行为会受到限制:
Cookie、LocalStorage
和IndexDB
无法读取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
了。
注意,这种方法只是用于Cookie
和iframe
,LocalStorage
和IndexDB
无法通过这种方法规避同源政策,而是要是用PostMessage API
,下面我们会介绍。
二、iframe
如果两个网页不同源,就没法拿到对方的DOM
。典型的例子是iframe
窗口和用window.open
方法打开的窗口,它们与父窗口无法通信。
所以对于完全不同源的网站,目前可以使用一下三种办法规避同源问题:
- 片段标识符(
fragment identifier
) window.name
- 跨文档通信
API
(window.postMessage
)
1.片段标识符
片段标识符指的是URL
中#
后面的内容,比如http://sillywa.com/a.html#fragment
中的#fragment
,如果只是改变片段标识符,页面不会重新刷新。
父窗口可以把信息写入子窗口的片段标识符:
1 | var src = originURL + '#' + data |
子窗口通过监听hashchange
事件得到通知:
1 | window.onhashchange = function() { |
2.window.name
浏览器窗口有window.name
属性。这个属性的最大特点是,无论是否同源,只要在同一个窗口里,前一个网页设置了这个属性,后一个网页可以读取它。
3. window.postMessage
HTML5
为了解决跨窗口通讯问题引入了一个新的API
:跨文档通信API
。这个API
为window
新增了一个window.postMessage()
方法,允许跨窗口通讯,不论这两个窗口是否同源。举例来说:假设父窗口为:http://aaa.com
,子窗口为:http://bbb.com
1 | // 父窗口向子窗口发送消息 |
postMessage()
方法的第一个参数是具体的信息内容,第二个参数是接收消息的窗口的源(origin
),即”协议 + 域名 + 端口”。也可以设为*,表示不限制域名,向所有窗口发送。
同样,子窗口向父窗口发送消息可以这样写:
1 | window.opener.postMessage('Nice to see you', 'http://aaa.com'); |
父窗口和子窗口都可以通过message
事件,监听对方的消息:
1 | window.addEventListener('message', function(e) { |
message
事件的event
对象有以下三个属性:
event.source:
发送消息的窗口event.origin:
消息发送的网址event.data:
消息内容
下面的例子是,子窗口通过event.source
属性引用父窗口,然后发送消息。
1 | window.addEventListener('message', receiveMessage); |
如果我们将发送的消息改为LocalStorage
,则可以互相读取LocalStorage
。
三、AJAX
同样AJAX
请求也会受到同源策略的影响,除了使用代理服务器外,还有一下方法可以实现跨域:
jsonp
WebScoket
CORS
1.jsonp
jsonp
想必大家都很了解,其由两部分组成:回调函数和数据。其基本思路是:动态插入script
标签,向服务器请求json
数据,返回的数据将在回调函数里获得。
1 | function addScriptTag(src) { |
上面代码通过动态添加<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 | var scoket = new WebScoket("ws://www.example.com/server.php") |
因为WebScoket
只能发送纯文本数据,所以对于复杂的数据类型我们应先将其序列化转化为json字符串
1 | var message = { |
同样服务器必须先解析再读取数据。
当服务器向客户端发来消息时,WebScoket
对象就会触发message
事件。这个message
事件与其它传递消息的协议类似,也就是把返回的数据保存在event.data
的属性中。
1 | scoket.onmessage = function(event) { |
与通过send()
发送到服务器的数据一样,event.data
中返回的数据也是字符串。
WebScoket
对象还有其他三个事件,在连接生命周期的不同阶段触发。
open:
在成功建立连接时触发error
:在发生错误时触发,连接不能持续close:
在连接关闭时触发
WebScoket
对象不支持DOM2
级事件侦听器,因此必须使用DOM0
级语法分别定义每个事件处理程序。
1 | var scoket = new WebScoket("ws://www.example.com/server.php") |
在这三个事件中只有close
的event
对象有额外的信息。这个事件的对象有三个额外的属性: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高级程序设计》