同源策略
同源
- 协议相同
- 域名相同
- 端口相同
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的浏览器都会使用这个策略。如果非同源,共有三种行为受到限制:
- Cookie、LocalStorage和IndexDB无法读取;
- DOM无法获得;
- AJAX请求不能得到正确响应。
同源策略的目的
如果没有同源策略,则可能发生以下情况:
- 一个黑客,利用iframe将真正的银行登录页面嵌入自己的页面,当使用真正的用户名、密码登录时,他的页面就可以通过JavaScript读取到表单中input的内容,获取用户名和密码;
- CSRF攻击。恶意网站暗自访问正确网站的api,如转账等。我们发起的HTTP请求会全额发送request地址对应的cookie,因为同浏览器而带有正确网站的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 | document.getElementById('myIFrame').contentWindow.document |
上面的命令中,父窗口想获取子窗口的DOM,因为跨源导致报错;反之亦然,子窗口获取父窗口的DOM也会报错:
1 | window.parent.document.body |
如果两个窗口一级域名相同,只是二级域名不同,那么设置document.domain
属性,就可以规避同源策略,拿到DOM。
对于完全不同源的网站,目前有三种方法,可以解决跨域窗口的通信问题:
- 片段识别符(fragment identifier)
- window.name
- 跨文档通信API(Cross-document messaging)
片段标识符
片段标识符指的是,URL的#号后面的部分,比如http://example.com/x.html#fragment
的#fragment
。如果只是改变片段标识符,页面不会重新刷新。
父窗口可以将信息写入子窗口的片段标识符:
1 | var src = originURL + '#' + data |
子窗口通过监听haschange事件得到通知:
1 | // 当一个窗口的哈希(location.hash)改变就会触发haschange事件 |
同样的,子窗口也可以改变父窗口的片段标识符:
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 | var popup = window.open('http://bbb.com', 'title') |
父窗口和子窗口都可以通过message
事件,监听对方的消息:
1 | window.addEventListener('message', function (e) { |
message事件的事件对象event
message
事件的事件对象event
,提供以下三个属性:
- event.source:发送消息的窗口;
- event.origin:发送者的源(可以判断非法发送者);
- event.data:消息内容。
子窗口通过event.source
属性引用父窗口,然后发送消息:
1 | window.addEventListener('message', function (e) { |
event.origin
属性可以过滤掉不受信的发送者:
1 | window.addEventListener('message', function (e) { |
读写LocalStorage
通过window.postMessage,读写其他窗口的LocalStorage也成为了可能。
主窗口写入iframe子窗口的LocalStorage:
1 | window.onmessage = function (e) { |
AJAX
除了架设服务器代理(浏览器请求同源服务器,再由后者请求外部服务),有三种方法规避这个限制:
- JSONP
- WebSocket
- CORS
JSONP
JSONP是服务器与客户端跨域通信的常用方法,最大的特点是简单适用,老式浏览器全部支持,服务器改造非常小。
它的基本思想是,网页通过添加一个<script>
元素,而<script>
元素的src属性是不受同源策略限制的;服务器收到请求后,将数据放在一个指定名字的回调函数里传回来。
首先,网页动态插入<script>
元素,由它向跨域网址发出请求:
1 | function addScriptTag (src) { |
上面代码通过动态添加<script>
元素,向服务器example.com
发出请求。注意,该请求的查询字符串有一个callback
参数,用来指明回调函数的名字,这对JSONP是必需的。
假设客户端期望返回JSON数据:{ "ip": "8.8.8.8" }
,实际服务器收到这个请求以后,会将数据放到回调函数的参数位置返回:
1 | foo({ |
因为其原理,显然,JSONP只能进行GET请求。
WebSocket
WebSocket是一种通信协议,使用ws://
(非加密)和wss://
(加密)作为协议前缀。该协议不实行同源策略,只要服务器支持,就可以通过它进行跨域通信。
下面是一个例子,浏览器发出的WebSocket请求的头信息:
1 | GET /chat HTTP/1.1 |
上面代码中,有一个字段是Origin
,表示该请求的请求源,即发自哪个域名。正因为有了Origin
这个字段,所以WebSocket才没有实行同源策略。因为服务器可以根据这个字段,判断是否许可本次通信。
CORS
跨域资源共享标准新增了一组HTTP首部字段,允许服务器声明哪些源站有权限访问哪些资源。另外,规范要求,对那些可能对服务器数据产生副作用的HTTP请求方法(特别是GET以外的HTTP请求,或者搭配某些MINE类型的POST请求),浏览器必须首先使用OPTIONS方法发起一个预检请求(preflight request),从而获知服务器是否允许该跨域请求。服务器确认允许之后,才发起实际的HTTP请求。在预检请求的返回中,服务器端也可以通知客户端,是否需要携带身份凭证(包括Cookies和HTTP相关数据)。
参考文献
- 阮一峰,浏览器同源政策及其规避方法