跨域问题

什么是跨域

作为前端开发,尤其当下前后端分离越来越普遍,跨域成为我们工作中经常会遇到的问题。

跨域,是指浏览器不能执行其他网站的脚本。它是由浏览器的同源策略造成的,是浏览器对JavaScript实施的安全限制。所谓的同源是指 域名(包括二级域名)、协议、端口均相同。

localhost 调用 127.0.0.1 也属于跨域。

同源策略限制了以下行为:

  • 无法读取 CookieLocalStorageSessionStorageIndexDB
  • 无法获取 DOMJS 对象
  • 无法发送 Ajax 请求

常见跨域场景主有二种:

  • 场景一:嵌套第三方页面,并互相通信。
  • 场景二:跨域Ajax请求。

场景一主要涉及的前端技术有:

  • Iframe
  • PostMessage
  • 代理服务(Nginx

场景二主要涉及的前端技术有:

  • JSONP
  • 跨域资源共享(Cors)
  • 代理服务(Nginx

接下来,我们分别探讨一下。

Iframe

Iframe 的应用一直非常广泛,包括当下。用于在你的页面开辟一个子窗体嵌套展示其他页面。主要解决页面级别的复用问题。很多时候,你不仅需要展示子页面,还需要和子页面沟通。由于Iframe同样受同源策略限制,跨域问题就随之而来。

曾经我们主要通过以下2种方式解决iframe跨域问题:

  • 设置 domain 解决主域名相同,二级域名不同导致的跨域阻碍。
  • 通过一个同域中间页

当下,我们主要通过接下来要介绍的 PostMessage来解决跨域窗口通信问题,以上方式就不具体赘述。

PostMessage

window.postMessage是一个安全的、基于事件的消息API。它允许来自不同源的脚本采用异步方式进行有限的通信,可以实现跨文档、多窗口、跨域消息传递。IE8+chromefirefox等都已经支持。同源窗口通信也可以使用PostMessage,逻辑比较清晰。

PostMessage实现通信的方式也很简单,分为两步:

  • 发送消息
  • 接受消息

发送消息

在需要发送消息的源窗口调用targetWindow.postMessage(message,targetOrigin)方法即可发送消息:

targetWindow

targetWindow是对目标窗体的引用。获得该引用的方法包括:

  • Window.open JS打开的窗体
  • Window.opener 打开当前窗体的窗体
  • HTMLIFrameElement.contentWindow iframe窗体
  • Window.parent 当前窗体的父窗体
  • Window.frames[index] 当前窗体的 iframe

message 参数

  • 作用:要传递的数据。
  • 类型:可以是JS的任意基本类型或可复制的对象。然而部分浏览器只能处理字符串参数,保险起见,推荐使用JSON.stringify()方法对数据序列化。

targetOrigin 参数:

  • 类型:string
  • 作用:为了安全考虑,指明目标窗口的源,协议+域名+端口号[+path],path会被忽略,所以可以不写。当然如果愿意也可以设置为"*",这样可以传递给任意窗口,如果要指定和当前窗口同源的话可设置为"/"

只有当目标窗口的源与postMessage函数中传入的源参数值匹配时,才能接收到消息。

举个栗子:

// http://www.domainA.com
// 发送消息
var iframe = document.getElementById('iframe')
iframe.contentWindow.postMessage('hi', 'http://www.domainB.com')

接收消息

目标窗体通过监听windowmessage事件就可以接收任何窗口传递来的消息了。

message事件的event对象有三个属性,分别是:

  • event.data 表示接收到的消息;
  • event.origin 表示postMessage的发送来源,包括协议,域名和端口;
  • event.source 表示发送消息的窗口对象的引用。我们可以用这个引用来建立两个不同来源的窗口之间的双向通信。

举个栗子:

// http://www.domainB.com
// 接受消息
function receiveMsg(event) {
  // 打印消息
  console.log(event.data)
  // 双向通信
  event.source.postMessage('hello', 'http://www.domainA.com')
  // 如果是父窗口 也可以这么沟通
  window.parent.postMessage('hello', 'http://www.domainA.com')
}
// 监听message事件
if (window.addEventListener) {
  window.addEventListener('message', receiveMsg, false);
}else {
  window.attachEvent('message', receiveMsg);
}

JSONP

曾经主流的跨域Ajax请求的主要方式,虽然当下已经有点过时,但还是值得了解以下的。它的原理比较有趣,算是一种“投机取巧”的设计模式吧。

  • 原理:利用html页面允许通过相应的标签从不同域名下加载静态资源文件是被浏览器允许的特点,动态的创建script标签,再去请求一个带参(包含一个回调方法作为参数)url来实现跨域通信。
  • 缺点:只能够实现 get 请求。
//原生实现方式
let script = document.createElement('script');

script.src = 'http://www.nealyang.cn/login?username=Nealyang&callback=callback';

document.body.appendChild(script);

function callback(res) {
  console.log(res);
}

跨域资源共享(Cors)


CORS 是目前主流的跨域 Ajax 请求解决方案。

  • 是一个W3C标准,全称是”跨域资源共享”(Cross-origin resource sharing)。
  • 它允许浏览器向跨源服务器,发出XMLHttpRequest请求,从而克服了AJAX只能同源使用的限制。
  • 需要浏览器和服务器同时支持。目前,所有浏览器都支持该功能,IE浏览器不能低于IE10
  • 整个CORS通信过程,都是浏览器自动完成,不需要用户参与。
  • 实现CORS通信的关键是服务器。只要服务器实现了CORS接口,就可以跨源通信。浏览器一旦发现AJAX请求跨源,就会自动添加一些附加的头信息,有时还会多一次附加的请求,但用户不会有感觉。

要理解Cors 需要搞清楚以下几个概念:

简单请求

简单请求需要满足以下几点:

  • 请求方式为 HEAD、POST 或者 GET
  • http头信息不超出以下原始字段:Accept、Accept-Language 、 Content-Language、 Last-Event-ID、 Content-Type。
  • Content-Type限于三个值:application/x-www-form-urlencoded、multipart/form-data、text/plain

非简单请求

简单请求之外的其他请求。非简单请求在正式通信之前,浏览器会先发送OPTION请求,进行预检,这一次的请求称为“预检请求”。服务器成功响应预检请求后,才会发送真正的请求,并且携带真实数据。

withCredentials 属性

CORS请求默认不发送Cookie和HTTP认证信息。如果要把Cookie发到服务器,一方面要服务器同意,指定Access-Control-Allow-Credentials字段。另一方面,开发者必须在AJAX请求中打开withCredentials属性。

代理服务(如 Nginx)

  • 特殊,个别,紧急情况下,可以考虑通过代理服务(如Nginx)配置url地址映射解决跨域等问题。
  • 一般由运维/后端人员负责配置。
  • 不用发布代码,高效,非常有用。

参考资料

详解跨域
阮一峰-Cors详解
Cors常见问题