跨域

同源策略

同源

  • 协议相同
  • 域名相同
  • 端口相同

http://www.example.com/dir/page.html,协议是http://,域名是www.example.com,端口是80

  • http://www.example.com/dir2/other.html:同源;
  • http://example.com/dir/other.html:不同源,域名不同;
  • http://v2.example.com/dir/other.html:不同源,域名不同;
  • http://www.example.com:81/dir/other.html:不同源,端口不同;
  • https://www.example.com/dir/other.html:不同源,协议不同。

同源策略

同源策略(Same Origin Policy)是有Netscape提出的一个著名的安全策略,所有支持JavaScript的浏览器都会使用这个策略。如果非同源,共有三种行为受到限制:

  1. Cookie、LocalStorage和IndexDB无法读取;
  2. DOM无法获得;
  3. AJAX请求不能得到正确响应。

同源策略的目的

如果没有同源策略,则可能发生以下情况:

  1. 一个黑客,利用iframe将真正的银行登录页面嵌入自己的页面,当使用真正的用户名、密码登录时,他的页面就可以通过JavaScript读取到表单中input的内容,获取用户名和密码;
  2. CSRF攻击。恶意网站暗自访问正确网站的api,如转账等。我们发起的HTTP请求会全额发送request地址对应的cookie,因为同浏览器而带有正确网站的cookie,获得权限。

规避同源策略的限制

Cookie是服务器写入浏览器的一小段信息,只有同源的网页才能共享。但是,两个网页一级域名相同,只是二级域名不同,浏览器允许通过document.domain共享Cookie。

举例来说,A网页是http://w1.example.com/a.html,B网页是http://w2.example.com/b.html,那么只要设置相同的document.domain,两个网页就可以共享Cookie:document.domain='example.com'

现在,A网页通过脚本设置一个Cookie:document.cookie = 'test1=hello',B网页就可以读到这个Cookie:var cookie = document.cookie

注意,这种方法只适用于Cookie和iframe窗口(目前知道适用iframe的Cookie和取得DOM),LocalStorage和IndexDB无法通过这种方法规避同源策略。

另外,服务器也可以在设置Cookie的时候,指定Cookie的所属域名为一级域名,比如:Set-Cookie: key=value; domain=.example.com; path=/,这样,二级域名和三级域名不用做任何设置,都可以读取这个Cookie。

iframe

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

比如,父窗口运行下面的命令,如果iframe窗口不是同源,就会报错:

1
2
document.getElementById('myIFrame').contentWindow.document
// Uncaught DOMException: Blocked a frame from accessing a cross-origin frame.

上面的命令中,父窗口想获取子窗口的DOM,因为跨源导致报错;反之亦然,子窗口获取父窗口的DOM也会报错:

1
2
window.parent.document.body
// 报错

如果两个窗口一级域名相同,只是二级域名不同,那么设置document.domain属性,就可以规避同源策略,拿到DOM。

对于完全不同源的网站,目前有三种方法,可以解决跨域窗口的通信问题:

  • 片段识别符(fragment identifier)
  • window.name
  • 跨文档通信API(Cross-document messaging)

片段标识符

片段标识符指的是,URL的#号后面的部分,比如http://example.com/x.html#fragment#fragment。如果只是改变片段标识符,页面不会重新刷新。

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

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

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

1
2
3
4
5
// 当一个窗口的哈希(location.hash)改变就会触发haschange事件
window.onhaschange = checkMessage
function checkMessage () {
var message = window.location.hash
}

同样的,子窗口也可以改变父窗口的片段标识符:

1
parent.location.href = target + '#' + hash

window.name

浏览器窗口有window.name属性。这个属性最大的特点是,无论是否同源,在同一个窗口里,页面设置了这个属性,即使页面跳转也不会改变。

父窗口先打开一个子窗口,载入一个不同源的网页,该子窗口的网页将信息写入window.name属性:window.name = data

接着,子窗口跳回一个与主窗口同域的网址:location = 'http://parent.url.com/xxx.html'

然后,主窗口就可以读取子窗口的window.name了:var data = document.getElementById('myFrame').contentWindow.name

这种方法的优点是,window.name容量很大,可以放置非常长的字符串;缺点是必须监听子窗口window.name属性的变化,影响网页性能。

window.postMessage

定义

上面两种方法都属于破解。HTML5为了解决这个问题,引入了一个全新API:跨文档通信API(Cross-document messaging)。

这个API为window对象新增了一个window.postMessage方法,允许跨窗口通信,不论这两个窗口是否同源。

我认为阮一峰的博客有问题,暂且写成我认为正确的样子:

举例来说,父窗口http://aaa.com向子窗口http://bbb.com发消息,调用postMessage就可以了:window.opener.postMessage('Nice to meet you', 'http://bbb.com)

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

子窗口向父窗口发送消息的写法类似:

1
2
var popup = window.open('http://bbb.com', 'title')
popup.postMessage('Hello World!', 'http://aaa.com')

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

1
2
3
window.addEventListener('message', function (e) {
console.log(e.data)
})
message事件的事件对象event

message事件的事件对象event,提供以下三个属性:

  • event.source:发送消息的窗口;
  • event.origin:发送者的源(可以判断非法发送者);
  • event.data:消息内容。

子窗口通过event.source属性引用父窗口,然后发送消息:

1
2
3
window.addEventListener('message', function (e) {
e.source.postMessage('Nice to meet you!', '*')
})

event.origin属性可以过滤掉不受信的发送者:

1
2
3
4
window.addEventListener('message', function (e) {
if (e.origin !== 'http://aaa.com') return
console.log(e.data)
})
读写LocalStorage

通过window.postMessage,读写其他窗口的LocalStorage也成为了可能。

主窗口写入iframe子窗口的LocalStorage:

1
2
3
4
window.onmessage = function (e) {
var payload = JSON.parse(e.data)
localStorage.setItem(payload.key, JSON.stringify(payload.data))
}

AJAX

除了架设服务器代理(浏览器请求同源服务器,再由后者请求外部服务),有三种方法规避这个限制:

  • JSONP
  • WebSocket
  • CORS

JSONP

JSONP是服务器与客户端跨域通信的常用方法,最大的特点是简单适用,老式浏览器全部支持,服务器改造非常小。

它的基本思想是,网页通过添加一个<script>元素,而<script>元素的src属性是不受同源策略限制的;服务器收到请求后,将数据放在一个指定名字的回调函数里传回来。

首先,网页动态插入<script>元素,由它向跨域网址发出请求:

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)
}

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

function foo (data) {
console.log('Your public IP address is: ' + data.ip)
}

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

假设客户端期望返回JSON数据:{ "ip": "8.8.8.8" },实际服务器收到这个请求以后,会将数据放到回调函数的参数位置返回:

1
2
3
foo({
"ip": "8.8.8.8"
})

因为其原理,显然,JSONP只能进行GET请求。

WebSocket

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

下面是一个例子,浏览器发出的WebSocket请求的头信息:

1
2
3
4
5
6
7
8
GET /chat HTTP/1.1
Host: server.example.com
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Key: x3JJHMbDL1EzLkh9GBHXDw==
Sec-WebSocket-Protocol: chat, superchat
Sec-WebSocket-Version: 13
Origin: http://example.com

上面代码中,有一个字段是Origin,表示该请求的请求源,即发自哪个域名。正因为有了Origin这个字段,所以WebSocket才没有实行同源策略。因为服务器可以根据这个字段,判断是否许可本次通信。

CORS

跨域资源共享标准新增了一组HTTP首部字段,允许服务器声明哪些源站有权限访问哪些资源。另外,规范要求,对那些可能对服务器数据产生副作用的HTTP请求方法(特别是GET以外的HTTP请求,或者搭配某些MINE类型的POST请求),浏览器必须首先使用OPTIONS方法发起一个预检请求(preflight request),从而获知服务器是否允许该跨域请求。服务器确认允许之后,才发起实际的HTTP请求。在预检请求的返回中,服务器端也可以通知客户端,是否需要携带身份凭证(包括Cookies和HTTP相关数据)。

参考文献