浏览器同源政策及其规避方法

1.浏览器同源策略

所谓"同源"指的是三个相同:

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

同源政策的目的,是为了保证用户信息的安全,防止恶意的网站窃取数据。

在非同源的情况下,有三种行为受到限制:

  1. Cookie、LocalStorage和IndexedDB无法读取;
  2. DOM无法获得;
  3. AJAX请求不能发送。

2.Cookie

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

但需要注意的是,这种方法只适用于 Cookie 和 iframe 窗口,LocalStorage和IndexedDB无法通过这种方法,规避同源政策,而要使用下文介绍的PostMessage API。

另外,服务器可以在设置Cookie的时候,指定Cookie的所属域名为一级域名,比如example.com。

Set-Cookie: key=value; domain=.example.com; path=/

这样的话,二级域名和三级域名不用做任何限制,都可以读取这个Cookie。

3.iframe

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

<iframe id="myIFrame" src="http://www.baidu.com" frameborder="0">百度一下</iframe>
<script>
  //父窗口访问iframe,如果iframe不同源会报错
  document.getElementById('myIFrame').contentWindow.document
  
  //iframe访问父窗口,如果iframe不同源会报错
  window.parent.document.body

如果两个窗口一级域名相同,只有二级域名不同,那么设置上一节介绍的document.domain属性,就可以规避同源政策,拿到dom。

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

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

3.1 片段识别符

片段识别符(fragment identifier)指的是,URL的#号后面的部分,比如http://www.baidu.com/index#fragment的#fragment。如果只是改变片段识别符,页面不会重新刷新。

父窗口可以把信息,写入子窗口的片段识别符。

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

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

window.onhashchange = checkMessage;

function checkMessage() {
  var message = window.location.hash;
  // ...
}

子窗口也可以改变父窗口的片段标识符。

parent.location.href= target + "#" + hash;

3.2 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属性的变化,影响网页性能。

3.3 window.postMessage

上面的两种方法都属于破解,HTML5为了解决这个问题,引入了一个全新的API:跨文档通信API(Cross-document API)。这个API为windom对象新增了一个window.postMessage方法,允许跨窗口通信,不论这两个窗口是否同源。

父窗口向子窗口发送数据。

  window.addEventListener('message', e=>console.log(e.data))
  let child = open('postMessage2.html')
  setTimeout(()=>child.postMessage('你好', '*'), 2000)

子窗口向父窗口发送数据。

  window.addEventListener('message', e=>console.log(e.data))
  window.opener.postMessage('你也好','*')

监听消息。

window.addEventListener('message', e=>console.log(e.data))

4.AJAX

同源策略规定,AJAX请求只能发给同源的网址,否则就会报错。

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

  1. JSONP
  2. WebSocket
  3. CORS

4.1 JSONP

JSONP(JSON with padding),是服务器和客户端跨源通信的常用方法。

JSONP的基本思想是,网页通过添加一个<script>标签,向服务器请求JSON数据,服务器收到请求后,将数据放在一个指定名字的回调函数里传回来。这种做法利用了<script>标签引用非同源脚本不受限制的特性,规避了同源策略。

4.2 WebSocket

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

4.3 CORS

CORS(Cross-origin resource sharing)是跨源AJAX请求的根本解决方法。它允许浏览器向跨源服务器,发送XMLHttpRequest请求,从而克服了AJAX只能同源使用的限制。相比JSONP只能发GET请求,CORS允许任何类型的请求。

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

只要同时满足以下两大条件,就属于简单请求:

(1)请求方法是GET、HEAD、POST之一;

(2)HTTP的头信息不超出以下几种字段:

  • Accept
  • Accept-Language
  • Content-Language
  • Last-Event-ID
  • Content-Type:只限于三个值application/x-www-form-urlencodedmultipart/form-datatext/plain

凡是不满足这两个要求的请求,就属于非简单请求。

(1)简单请求

对于简单请求,浏览器直接发出CORS请求。具体来说,就是在头信息中,增加一个Origin字段。

GET /cors HTTP/1.1
Origin: http://api.bob.com
Host: api.alice.com
Accept-Language: en-US
Connection: keep-alive
User-Agent: Mozilla/5.0...

上面的头信息中,Origin用来说明,本次请求来自哪个源(协议 + 域名 + 端口)。服务器根据这个值,决定是否同意这次请求。

(2)非简单请求

非简单的CORS请求,会在正式发送之前,增加一次HTTP查询请求,称为预设请求。

浏览器先询问服务器,当前网页所在的域名是否在服务器的许可名单之中,以及可以使用哪些HTTP动词和头信息字段。只有得到肯定答复,才会发送CORS请求,否则就报错。

posted @ 2020-09-12 20:40  心流flux  阅读(492)  评论(0)    收藏  举报