跨域相关解决方案整理

前段时间一直在面试求职,因为自己没有做足面试前的准备,所以导致面试过程中的表现不是特别理想。虽然现在找到工作了,但是既然知道自己存在 bug,还是要把它修了为下一次面试做准备!

为什么要写这篇关于跨域的文章?

之前一直认为自己对跨域这块知识点的掌握足以应付面试,但面试之后才发现高估了自己。跨域这块的知识点自己掌握得相当模糊,虽然自己心里可能知道是跨域相关解决方案的原理,但是要我用语言表达出来就说得模棱两可,而且 10 家公司的前端面试基本上就有 6 家会问你关于跨域的问题,所以如果你也正在准备前端职位的面试,那这块你必须得攻下,至少要能够回答得上面试官经常会问的几个点。

什么是跨域?

要理解什么是跨域,首先要知道同源策略。浏览器厂商为了防止安全问题的发生,推出了同源策略(same-origin policy),限制当前访问的网页向另外一个网页发送请求获取数据(包括访问另外一个网页的 cookie)。试想一下你同时访问银行网页 A 和普通网页 B,如果网页 B 可以直接访问网页 A 储存在本地的 cookie 数据,甚至直接向 A 发送请求获取相关数据,这会非常危险。

同源策略规定浏览器只有同时满足以下 3 点才能允许当前网页向另外一个网站发送请求:

  1. 协议相同;
  2. 域名相同;
    • 二级域名不同也会被认为是不同源被禁止;
    • 使用域名对应的 IP,也会被认为是不同源。
  3. 端口相同。
    • Windows RT 系统的 IE 浏览器没有将这一点作为同源策略的组成部分,但这个例外是非标准的,其他浏览器没有这个例外。

如果不符合同源策略,以下三种行为会被禁止:

  1. 不能访问相关的本地存储数据,例如cookie,localStorage,IndexDB;
  2. 不能获取 DOM;
  3. 不能发起 AJAX 请求.

下面举个跨域请求的例子,网页 http://www.sample.com/index 将在浏览器请求以下 URL (或者访问 cookie,localStorage等等):

  • 请求 http://www.sample.com/category/one,符合同源策略,正常;
  • 请求 http://www.sample.com:81/category/one,端口不同,禁止;
  • 请求 http://sample.com/category/one,二级域名不同,禁止;
  • 请求 http://wap.sample.com/category/one,二级域名不同,禁止;
  • 请求 https://www.sample.com/category/one,协议不同,禁止。

为什么要跨域?

同源策略作为一个限制防止浏览器发生安全问题,但是实际项目开发中我们有时候确实需要绕过或者解除这个限制才能实现外部系统的对接,于是有了跨域的需求。例如你们公司旗下有产品 A 和产品 B,会安装部署在客户的 PC 上,绑定不同的域名。现在有一个简单的需求,产品 A 的网页上需要显示产品 B 的部分信息,对于这部分信息,产品 B 已经有相关的 Web API,产品 A 可以直接请求,这里就需要规避同源策略了。

跨域的解决方案

跨域的需求往往只是针对 AJAX 请求,但某些特殊情况下我们也需要访问 cookie 或者 localStorage 甚至获取某个 DOM,介于本篇文章的目的只是为了应付面试,所以不会有详细的使用介绍,但是会列举大多数解决方案以及相关原理,限制。

原理:相同一级域名的情况下,浏览器是允许互相访问 cookie 的,前提是 cookie 的 domain 属性跟域名相同,或者 cookie 的 domain 属性是当前页面的父域。这里涉及到 cookie 的基本概念和访问权限,可以自行 Google 进一步了解。

限制:只适用于一级域名相同的情况,而且无法共享 localStorage,只能是 cookie,如果一级域名也不相同,需要使用其他解决方案来间接实现 cookie 数据共享,例如 window.name,window.postMessage。

设置相同的 document.domain 让父页面和子页面(iframe 的形式)互相访问 DOM 元素

原理:浏览器是允许以 iframe 形式嵌入网页形成的父页面和子页面之间通过设置相同的 document.domain 来实现 DOM 元素的互相访问和操作的。

限制:这个方案的前提必须是以 iframe 的形式将子页面嵌入,而且一级域名相同。

设置 document.navigator 让不同 iframe 之间实现数据传递

原理:这似乎是浏览器的一个漏洞,无论是否同源,不同的 iframe 之间(包括父页面)的 document.navigator 对象是共享的,所以可以通过往 document.navigator 这个对象塞数据让不同 iframe 之间共享数据。

限制:这个方案的前提必须是以 iframe 的形式将子页面嵌入。

设置 window.name 让同一浏览器窗口的不同页面(即不同 Tab)实现数据传递

原理:无论是否同源,window.name 是同一浏览器窗口中的所有页面(即 Tab)所共享的,所以可以通过设置这个属性让不同页面实现数据传递。

限制:必须是同一浏览器窗口中的页面才能实现数据传递。

注意:window.name 还有一个在 iframe 场景的使用技巧,可以在 iframe 中嵌入的页面去修改 window.name,但是父页面读取/操作 iframe 里面的数据时,必须保证与父页面的域一样。那是不是父页面和 iframe 内嵌子页面的域不一样就不能用?不是的,因为 window.name 还有一个特性,就是在页面刷新之后的值不会被刷新重置,我们可以利用这个特性,在父页面的 iframe 加载完不同域的子页面之后,让 iframe 再次加载一个同域的子页面(我们可以专门制作一个空白的页面),而 window.name 的值没有因为刷新就被重置,这个时候我们就可以去读 window.name 的值了。

使用 window.postMessage 实现不同浏览器窗口(或不同 Tab)之间的消息传递

原理:window.postMessage 是 HTML5 新增的 API,其目的主要是解决浏览器不同窗口(或不同 Tab)之间消息传递的问题,但是由于不受同源策略的限制,我们也可以在某些场景用它来间接解决跨域的问题。

限制:凡是 HTML5 新增的 API,我们都要考虑其针对旧版本浏览器的兼容性,好在 window.postMessage 在 IE8 时就支持了,不过不同旧版本的浏览器写法可能会有不同,例如 Firefox 4.0 的版本就不支持 window.onmessage = function() {} 这样的写法,为了更好的兼容性最好使用事件绑定的写法。

监听 & 设置 URL 的片段识别符(“#”之后的内容)来实现数据传递

原理:URL 中 # 之后的部分可以通过 JavaScript 去设置而不会重新刷新页面,并且还可以设置监听事件来监听是否被更改来做进一步操作。

限制:不同浏览器对 URL 支持的长度不同,因为会造成存储在 URL 中的数据大小有限制,而且数据会暴露在 URL 中,也不美观。

动态创建 <script> 标签

原理:<script> 标签的 src 属性是不受同源策略限制的,通过动态创建 <script> 标签以 GET 的方式去请求指定的 URL,服务端那边返回的数据需要是 JavaScript 代码,可以是 JSON 结构的数据然后赋值给一个全局变量。因为返回的数据在浏览器这边会作为 JavaScript 代码执行,所以在动态加载完 <script> 后浏览器这边只需要访问之前约定好的变量即可。

限制:如果动态加载多个 <script> 标签可能会造成性能问题,所以在加载完 <script> 之后还需要将标签删除;还需要跟服务端的开发人员约定变量名,实现起来会有些繁琐,而且只支持 GET 方式请求。

JSONP(JSON with Padding)

原理:基本原理跟上面的 “动态创建 <script> 标签” 一样,也是利用 <script> 不受同源策略限制的特性,但需要在请求的 URL 中加入一个参数,服务端会用该参数的值作为方法名,将 JSON 数据作为这个方法的参数,以这样的格式返回给浏览器端,因为方法名可以在 URL 中自定义,相比上一种方法更加灵活优雅,而且对旧版本的浏览器兼容得好,在曾经一段时间这个解决方案非常流行。

限制:首先服务端返回的数据必须以 JSONP 的格式返回,浏览器端这边动态创建 <script> 标签和相关的解析逻辑比较复杂,不过可以通过使用相关的工具库来解决(jQuery 封装了对 JSONP 的请求方法);依然只能支持 GET 的请求方式。

同源服务器提供中转请求的 Web API

原理:同源策略毕竟是浏览器才存在的限制,如果我们在任何一门语言中使用代码去请求某个 URL,则不存在这个限制。所以可以提供一个与网页同源的 Web API,把请求另外的不同源的 Web API 让其代为请求,再将返回的数据一模一样的再返回给浏览器端。

限制:主要工作在同源的服务器端,会对同源服务端造成额外的开销。

使用 Nginx 等服务器软件做反向代理

原理:跟 “同源服务器提供中转请求的 Web API” 类似,只不过中转请求的实现逻辑不需要我们去管,服务器软件会做封装,只需要在服务器做相关配置即可。

限制:针对每个不同源的 Web URL 都需要根据域名在服务器上做相关配置,灵活性上稍微差一点。

WebSocket

原理:WebSocket 不受同源策略的限制。(对 WebSocket 不是很了解,这里只能简单说一句了)

限制:需要服务器和浏览器同时都支持 WebSocket 通讯协议。

跨域资源共享 CORS(Cross-origin resource sharing)

原理:CORS 机制的诞生是为了给解决跨域问题提供一个统一的标准,相关原理比较复杂,可以自行查阅 MDN 的文档,里面说得非常详细。不过面试时倒也不会问得非常深入,只要理解下面 3 点我觉得面试应该就没问题了。

  1. 三个基本使用场景:简单请求,预请求,附带身份凭证的请求;
  2. CORS 几个主要是头部参数,例如 Access-Control-Allow-Origin,Access-Control-Request-Method等;
  3. CORS 的优点(支持所有请求方式,灵活,跨域解决方案的标准)和目前阻碍 CORS 推广的因素(限制)。

限制:需要浏览器和服务器同时支持,IE 浏览器需要 10 以上(包括 10)才能兼容,不过 8 和 9 可以利用 XDomainRequest 来间接实现。

总结

以上列出的解决方案适用于不同场景,对于跨域发送请求的需求,以前使用得比较多的应该是 JSONP,而现在 CORS 已经成为这一领域的标准,因为用法更加灵活优雅且更安全;而对于多窗口通讯/iframe 内外通讯的需求,以前用 window.name 可能更加多一些因为使用简单,当时也没有统一的解决方案,而现在自然是使用 window.postMessage 这个优雅的写法了。

本文链接:https://blog.wardchan.com/posts/cross-origin-solution-summary.html参与评论 »

--EOF--

Comments