浏览器同源策略及 Ajax 跨域解决方案

565 阅读7分钟

因为在开发过程中会经常遇到因为浏览器同源策略而导致的跨域问题,而多数开发者对浏览器同源策略和跨域问题并没有很清晰的认识,所以打算在这篇文章中说下浏览器同源策略和我们最经常会遇到的 Ajax 跨域问题及其解决方案。

对于源的定义,MDN 中是这么解释的:如果两个页面的协议、域名和端口都相同,则两个页面具有相同的源。

从定义我们可以知道,关注两个页面是否同源,只要比较两个页面的协议域名端口即可。

举个例子,假设有以下页面,比较 A 页面与其它页面是否同源~

A:http://xys.ttsy/a.html 
B:http://xys.ttsy/b.html 
C:https://xys.ttsy/c.html 
D:http://d.xys.ttsy/d.html 
E:http://xys.ttsy:8081/e.html 

根据定义,可以知道 A 和 B 同源,而 A 和 C、D、E 不同源。A、B 页面同源是因为其协议(都是 http)、域名(都是 xys.ttsy)和端口(都是 80)都相同;而 A 与 C、D、E 不同源,是因为 A 和 C 不同协议(http 和 https),A 和 D 不同域名(xys.ttsy 和 d.xys.ttsy),A 和 E 不同端口(80 和 8081) 。

在浏览器中,一个最核心也最基本的安全功能便是同源策略。

同源策略是指浏览器中一个源的脚本只能访问同源的另一个脚本的策略。

也就是说,在浏览器中的脚本如果要访问其它脚本的话,那么两个脚本必须是同源的,否则会受到浏览器同源策略的限制。如果两个脚本非同源,会有三个行为受到限制

  • DOM 无法获得;
  • Cookie、LocalStorage 和 IndexDB 无法共享;
  • Ajax 请求限制;

DOM 无法获得

DOM 无法获得的限制最常见的是在 iframe 窗口与父窗口之间,如果父窗口与其 iframe 窗口的脚本是不同源的,则它们互相无法获取对方的 DOM 元素。

如下父窗口与 iframe 不同源,父窗口中无法获取 iframe 窗口中脚本的 DOM 元素

<iframe id="myIframe" src="http://www.taobao.com" width="500" height="500" ></iframe>
<script type="text/javascript">
    var myIframe = document.getElementById('myIframe').contentWindow.document;
    console.log(myIframe)  // 能获取到 document 对象,但里面没有有效数据
    console.log(myIframe.body)  // 空的 body
</script>

上述代码打印出来的效果如下所示

无效的 document 和 body

同理,在 iframe 窗口脚本中也无法获取父窗口脚本的 DOM 元素

console.log(self.parent.document)
console.log(self.top.document)
console.log(self.parent.document.body)
console.log(self.top.document.body)

那么,有没有什么方式能够规避此类同源策略的限制呢?答案是有的,只是这其中也是有条件的。

当父窗口与 iframe 窗口一级域名相同而二级域名不同的时候(或者说有共同的一级域名更为准确一点),则可以通过设置 document.domain 属性来规避此类同源策略的限制。

只要分别在两个窗口中对应的脚本文件中设置如下代码即可

document.domain='ttsy.com';  // 设置同一个一级域名

Cookie、LocalStorage 和 IndexDB 无法共享

若两个脚本不同源,则 Cookie、LocalStorage 和 IndexDB 的内容无法共享。

对于 Cookie 来说,有两种方式可以规避此类同源策略的限制。

若是一级域名相同而二级域名不同的情况(或者说有共同的一级域名更为准确一点),则可以通过设置 document.domain 属性来规避此类同源策略的限制。

只要两个脚本文件设置相同的 document.domain 值,即可共享 Cookie 。

document.domain='ttsy';  // 设置相同的值

第二种方式则是服务器端代码在设置 Cookie 的时候,将 domian 属性设置为一级域名,那么该一级域名下的子域名同样可以共享 Cookie 。

而 LocalStorage 和 IndexDB 则无法通过上述方法来规避同源策略。

而对于完全不同源的页面来说,还可以通过 window.name、window.postMessage 等方式来实现通讯,本篇不继续赘述,有兴趣的童鞋可以查阅相关资料了解。

Ajax 请求限制

在一个脚本中,如果通过 Ajax 请求另一个非同源的脚本,则会报错。报错信息经常会类似下面酱紫

XMLHttpRequest cannot load xxx  . No 'Access-Control-Allow-Origin' header is present on the requested resource. Origin 'null' is therefore not allowed access.

这即是我们经常提到的 Ajax 跨域问题,这是由于浏览器的同源策略引起的,Ajax 无法请求非同源的资源。

对于 Ajax 跨域解决方案,通常来说有如下三种:

  • JSONP
  • CORS
  • 代理服务器

下面就详细描述上述三种 Ajax 跨域解决方案。

JSONP

JSONP 是解决跨域很常用的一种方法了,其原理是通过 script 标签向服务器端发起请求不受浏览器同源策略的限制。而服务器端在接受到请求后,可以返回一个指定名字的函数,该函数的参数是需要返回的数据。

举个例子吶~

var script = document.createElement('script');
script.setAttribute("type","text/javascript");
script.src = 'http://ttsy.com/getName?callback=fn';
document.body.appendChild(script);
    
function fn(data) {
    console.log('name: ' + data.name);
};

上述代码通过创建一个 script 标签,将其 src 属性设置为执行请求的 url 并将其插入到页面中,向服务器发起一个请求。上述代码中请求的 url 为 ttsy.com/getName?cal… ,指定了回调函数名为 fn 。

而服务端在收到该请求后返回一个指定名字的函数,该函数的参数是需要返回的数据。服务端返回数据如下

fn({
  "name": "ttsy"
  });

由于通过 script 标签请求到的数据会直接执行,所以上述服务端返回数据后会直接执行 fn 函数,所以在上述代码中 fn 函数将会输出 name:ttsy 。

基于 JSONP 的实现原理,其只能通过 get 请求来获得数据,不能进行较为复杂的 post 请求,所以在更多的情况下,我们会采用下面的 CORS 来解决 Ajax 的跨域问题。

CORS

CORS(Cross-origin resource sharing),全称是跨域资源共享。它允许 Web 应用服务器进行跨域访问控制,从而使跨域数据传输得以安全进行。是 Ajax 跨域问题的根本解决方案。

CORS 的实现需要浏览器与服务器共同支持。

目前来说,基本所有的浏览器都支持 CORS 功能,当浏览器发起 Ajax 跨域请求时,会在 HTTP 请求头添加一些附加的头信息,有时会多出一次 HTTP 请求,这些都是由浏览器自动完成的。

而在服务器中要实现了 CORS 功能,则需要设置 Access-Control-Allow-Origin 属性。

Access-Control-Allow-Origin: *

使用 CORS 解决跨域问题的话,会在发送请求时出现两种情况,分别为简单请求复杂请求

以 Ajax 为例,当满足以下条件时,会触发简单请求:

  1. Method 为 GET、POST 或 HEAD;
  2. Content-Type 为 text/plain、multipart/form-data 或 application/x-www-form-urlencoded;

若是不符合上述条件,则会触发复杂请求。对于复杂请求来说,首先会发起一个预检请求,该请求是 option 方法的,通过该请求来知道服务端是否允许跨域请求。

代理服务器

代理服务器是指通过设置一个同源的代理服务器,然后将我们的 Ajax 请求发送到我们的代理服务器上,再通过代理服务器去向实际的服务器去请求数据,最后通过代理服务器返回给浏览器。

通过设置代理服务器来规避浏览器同源策略的限制,其原理是服务器之间的资源请求并没有同源策略的限制。但是由于操作起来并不方便,所以在实际开发中并不会经常用这种方法来解决问题。