多是最好的跨域解决方案了... ...

今天咱们来聊一个老生常谈的话题,跨域!html

又是跨域,烦不烦 ?网上跨域的文章那么多,跨的我眼睛都疲劳了,不看了不看了 🤣 别走...我尽可能用最简单的方式将常见的几种跨域解决方案给你们阐释清楚,相信认真看完本文,之后不论是做为受试者仍是面试官,对于这块的知识都可以游刃有余。webpack

什么是“跨源”

不是讲跨域吗 ?怎么又来个“跨源” ?字都能打错的 ?nginx

😄...稍安勿躁,其实咱们日常说的跨域是一种狭义的请求场景,简单来讲,就是“跨“过浏览器的同源策略去请求资“源”,因此咱们叫它“跨源”也没啥问题。那么,git

跨源,源是什么?浏览器的同源策略github

什么是同源?协议,域名,端口都相同就是同源web

干巴巴的,能不能举个栗子?栗子:),有的有的面试

const url = 'https://www.google.com:3000'
复制代码

好比上面的这个 URL,协议是:https,域名是 www.google.com,端口是 3000json

不一样源了会怎么样?会有不少限制,好比设计模式

  • Cookie,LocalStorage,IndexDB 等存储性内容没法读取
  • DOM 节点没法访问
  • Ajax 请求发出去了,可是响应被浏览器拦截了

我就想请求个东西,至于吗,为何要搞个这么个东西限制我?基于安全考虑,没有它,你可能会遇到跨域

  • Cookie劫持,被恶意网站窃取数据
  • 更容易受到 XSS,CSRF 攻击
  • 没法隔离潜在恶意文件
  • ... ...

因此,得有。正是由于浏览器同源策略的存在,你的 Ajax 请求有可能在发出去后就被拦截了,它还会给你报个错:

✘ Access to XMLHttpRequest at 'xxx' from origin 'xxx' has been block by CORS,
  policy: No 'Access-Control-Allow-Origin' header is present on the requested resource.
复制代码

这种发出去拿不到响应的感觉,就像你在网上冲浪时,被一股神秘的东方力量限制了同样:

EC2519D9-DA3E-422F-B130-6120E846E088.png
很是难受,因此,咱们接下来就来看看怎么用科学的方法上网( 啊呸,科学的方法解决跨域的问题)。

JSONP

这玩意儿就是利用了 <script> 标签的 src 属性没有跨域限制的漏洞,让咱们能够获得从其余来源动态产生的 JSON 数据。

为何叫 JSONP ?JSONP 是 JSON with Padding 的缩写,额,至于为何叫这个名字,我网上找了下也没个标准的解释,还望评论区的各位老哥知道的赶忙告诉我: )

怎么实现 ?具体实现思路大体分为如下步骤

  • 本站的脚本建立一个 元素,src 地址指向跨域请求数据的服务器
  • 提供一个回调函数来接受数据,函数名能够经过地址参数传递进行约定
  • 服务器收到请求后,返回一个包装了 JSON 数据的响应字符串,相似这样:callback({...})

浏览器接受响应后就会去执行回调函数 callback,传递解析后的 JSON 对象做为参数,这样咱们就能够在 callback 里处理数据了。实际开发中,会遇到回调函数名相同的状况,能够简单封装一个 JSONP 函数:

function jsonp({ url, params, callback }) {
  return new Promise((resolve, reject) => {
    // 建立一个临时的 script 标签用于发起请求
    const script = document.createElement('script');
    // 将回调函数临时绑定到 window 对象,回调函数执行完成后,移除 script 标签
    window[callback] = data => {
      resolve(data);
      document.body.removeChild(script);
    };
    // 构造 GET 请求参数,key=value&callback=callback
    const formatParams = { ...params, callback };
    const requestParams = Object.keys(formatParams)
      .reduce((acc, cur) => {
        return acc.concat([`${cur}=${formatParams[cur]}`]);
      }, [])
			.join('&');
		// 构造 GET 请求的 url 地址
    const src = `${url}?${requestParams}`;
    script.setAttribute('src', src);
    document.body.appendChild(script);
  });
}

// 调用时
jsonp({
  url: 'https://xxx.xxx',
  params: {...},
  callback: 'func',
})
复制代码

咱们用 Promise 封装了请求,使异步回调更加优雅,可是别看楼上的洋洋洒洒写了一大段,其实本质上就是:

<script src='https://xxx.xxx.xx?key=value&callback=xxx'><script> 复制代码

想要看例子 ?戳这里

JSONP 的优势是简单并且兼容性很好,可是缺点也很明显,须要服务器支持并且只支持 GET 请求,下面咱们来看第二种方案,也是目前主流的跨域解决方案,划重点!😁

CORS

CORS(Cross-Origin Resource Sharing)的全称叫 跨域资源共享,名称好高大上,别怕,这玩意儿其实就是一种机制。浏览器不是有同源策略呐,这东西好是好,可是对于开发人员来讲就不怎么友好了,由于咱们可能常常须要发起一个 跨域 HTTP 请求。咱们以前说过,跨域的请求实际上是发出去了的,只不过被浏览器给拦截了,由于不安全,说直白点儿就是,你想要从服务器哪儿拿个东西,可是没有通过人家容许啊。因此怎么样才安全 ?服务器容许了不就安全了,这就是 CORS 实现的原理:使用额外的 HTTP 头来告诉浏览器,让运行在某一个 origin 上的 Web 应用容许访问来自不一样源服务器上的指定的资源

兼容性

目前,全部的主流浏览器都支持 CORS,其中,IE 浏览器的版本不能低于 10,IE 8 和 9 须要经过 XDomainRequest 来实现

完整的兼容性状况 ? 戳这里

实现原理

CORS 须要浏览器和服务器同时支持,整个 CORS 的通讯过程,都是浏览器自动完成。

怎么个自动法

简单来讲,浏览器一旦发现请求是一个跨域请求,首先会判断请求的类型

若是是简单请求,会在请求头中增长一个 Origin 字段,表示此次请求是来自哪个。而服务器接受到请求后,会返回一个响应,响应头中会包含一个叫 Access-Control-Allow-Origin 的字段,它的值要么包含由 Origin 首部字段所指明的域名,要么是一个 "*",表示接受任意域名的请求。若是响应头中没有这个字段,就说明当前源不在服务器的许可范围内,浏览器就会报错:

GET /cors HTTP/1.1
Origin: https://xxx.xx
Accept-Language: en-US
Connection: keep-alive
... ...
复制代码

若是是非简单请求,会在正式通讯以前,发送一个预检请求(preflight),目的在于询问服务器,当前网页所在的域名是否在服务器的许可名单之中,以及可使用哪些 HTTP 动词和头信息字段,只有获得确定答复,浏览器才会发出正式的请求,不然就报错。你可能发现咱们在平常的开发中,会看到不少使用 OPTION 方法发起的请求,它其实就是一个预检请求:

OPTIONS /cors HTTP/1.1
Origin: http://xxx.xx
Access-Control-Request-Method: PUT
Accept-Language: en-US
... ...
复制代码

那么到底哪些是简单请求,哪些是非简单请求 ?

请求类型

不会触发 CORS 预检的,就是简单请求。哪些请求不会触发预检 ?

使用如下方法之一:GET, HEAD, POST,

而且 Content-Type 的值仅限于下列三者之一:

  • text/plain
  • multipart/form-data
  • application/x-www-form-urlencoded

相反,不符合上述条件的就是非简单请求啦。

因此,实现 CORS 的关键是服务器,只要服务器实现了 CORS 的相关接口,就能够实现跨域。CORS 与 JSONP相比,优点是支持全部的请求方法,缺点是兼容性上较 JSONP 差。除了 JSONP 和 CORS 外,还有一种经常使用的跨域解决方案:PostMessage,它更多地用于窗口间的消息传递。

PostMessage

PostMessage 是 Html5 XMLHttpRequest Level 2 中的 API,它能够实现跨文档通讯(Cross-document messaging)。兼容性上,IE8+,Chrome,Firfox 等主流浏览器都支持,能够放心用😊,如何理解跨文档通讯?

你能够类比设计模式中的发布-订阅模式,在这里,一个窗口发送消息,另外一个窗口接受消息,之因此说相似发布-订阅模式,而不是观察者模式,是由于这里两个窗口间没有直接通讯,而是经过浏览器这个第三方平台。

window.postMessage(message, origin, [transfer])
复制代码

postMessage 方法接收三个参数,要发送的消息,接收消息的源和一个可选的 Transferable 对象,如何接收消息 ?

window.addEventListener("message", function receiveMessage(event) {}, false); // 推荐,兼容性更好

window.onmessage = function receiveMessage(event) {} // 不推荐,这是一个实验性的功能,兼容性不如上面的方法
复制代码

接收到消息后,消息对象 event 中包含了三个属性:source,origin,data,其中 data 就是咱们发送的 message。

此外,除了实现窗口通讯,postMessage 还能够同 Web Worker 和 Service Work 进行通讯,有兴趣的能够 戳这里

Websocket

Websocket 是 HTML5 的一个持久化的协议,它实现了浏览器与服务器的全双工通讯,同时也是跨域的一种解决方案。什么是全双工通讯 ?简单来讲,就是在创建链接以后,server 与 client 都能主动向对方发送或接收数据

原生的 WebSocket API 使用起来不太方便,咱们通常会选择本身封装一个 Websocket(嗯,咱们团队也本身封了一个 : ))或者使用已有的第三方库,咱们这里以第三方库 ws 为例:

const WebSocket = require('ws');

const ws = new WebSocket('ws://www.host.com/path');

ws.on('open', function open() {
  ws.send('something');
});

ws.on('message', function incoming(data) {
  console.log(data);
});
... ...
复制代码

须要注意的是,Websocket 属于长链接,在一个页面创建多个 Websocket 链接可能会致使性能问题。

Nginx 反向代理

咱们知道同源策略限制的是:浏览器向服务器发送跨域请求须要遵循的标准,那若是是服务器向服务器发送跨域请求呢?

答案固然是,不受浏览器的同源策略限制

利用这个思路,咱们就能够搭建一个代理服务器,接受客户端请求,而后将请求转发给服务器,拿到响应后,再将响应转发给客户端:

017D8331-6CD1-4E43-A912-7D21B7B04E45.png

这就是 Nginx 反向代理的原理,只须要简单配置就能够实现跨域:

# nginx.config
# ...
server {
  listen       80;
  server_name  www.domain1.com;
  location / {
    proxy_pass   http://www.domain2.com:8080;  #反向代理
    proxy_cookie_domain www.domain2.com www.domain1.com; #修改cookie里域名
    index  index.html index.htm;

    # 当用 webpack-dev-server 等中间件代理接口访问 nignx 时,此时无浏览器参与,故没有同源限制,下面的跨域配置可不启用
    add_header Access-Control-Allow-Origin *;
    add_header Access-Control-Allow-Credentials true;
    # ...
  }
}
复制代码

Node 中间件代理

实现的原理和咱们前文提到的代理服务器原理一模一样,只不过这里使用了 Node 中间件作为代理。须要注意的是,浏览器向代理服务器请求时仍然遵循同源策略,别忘了在 Node 层经过 CORS 作跨域处理:

const https = require('https')
// 接受客户端请求
const sever = https.createServer((req, res) => {
  ...
  const { method, headers } = req
  // 设置 CORS 容许跨域
  res.writeHead(200, {
    'Access-Control-Allow-Origin': '*',
    'Access-Control-Allow-Methods': '*',
    'Access-Control-Allow-Headers': 'Content-Type',
    ...
  })
  // 请求服务器
  const proxy = https.request({ host: 'xxx', method, headers, ...}, response => {
    let body = ''
    response.on('data', chunk => { body = body + chunk })
    response.on('end', () => {
      // 响应结果转发给客户端
      res.end(body)
    })
  })
})
复制代码

document.domain

二级域名相同的状况下,设置 document.domain 就能够实现跨域。什么是二级域名 ?

a.test.com 和 b.test.com 就属于二级域名,它们都是 test.com 的子域。如何实现跨域 ?

document.domain = 'test.com' // 设置 domain 相同

// 经过 iframe 嵌入跨域的页面
const iframe = document.createElement('iframe')
iframe.setAttribute('src', 'b.test.com/xxx.html')
iframe.onload = function() {
  // 拿到 iframe 实例后就能够直接访问 iframe 中的数据
  console.log(iframe.contentWindow.xxx)
}
document.appendChild(iframe)
复制代码

总结

固然,除了上述的方案外,比较 Hack 的还有:window.name, location.hash,可是这些跨域的方式如今咱们已经不推荐了,为何 ?由于相比之下有更加安全和强大的 PostMessage 做为替代。跨域的方案其实有不少,总结下来:

  • CORS 支持全部的 HTTP 请求,是跨域最主流的方案
  • JSONP 只支持 GET 请求,可是能够兼容老式浏览器
  • Node 中间件和 Nginx 反向代理都是利用了服务器对服务器没有同源策略限制
  • Websocket 也是一种跨域的解决方案
  • PostMessage 能够实现跨文档通讯,更多地用于窗口通讯
  • document.domain, window.name, location.hash 逐渐淡出历史舞台,做为替代 PostMessage 是一种不错的方案

写在最后

本文首发于个人 博客,才疏学浅,不免有错误,文章有误之处还望不吝指正!

若是有疑问或者发现错误,能够在评论区进行提问和勘误,

若是喜欢或者有所启发,欢迎 star,对做者也是一种鼓励。

(完)

相关文章
相关标签/搜索