你真的了解跨域吗

前言

相信每一个前端对于跨域这两个字都不会陌生,在实际项目中应用也不少,但跨域方法的多种多样让人应接不暇,前段时间公司同事出现了跨域问题,又一时找不到问题所在,因此在此总结下跨域知识,一篇由浅入深的万字Web基操文javascript

其实很早就开始写了,只不过刚开始写的时候理解不够深入,后来慢慢就写其余以为较高大尚较内涵的了,而后就又是以为不够完美不够深入又写一半,就此陷入强迫症患者明知不可为而为的死循环,SO,产出少,周期长(不过你们能看到的文章都是准备良久又反复斟酌后自认为还不错的)。。。css

总之又是一篇因为各类缘由半途而废的积压文,这里终于收尾了,长出一口气,哎,仍是太年轻,吐槽结束,进入正文html

文章收录地址: isboyjc/blog 传送门前端

什么是跨域

简单来讲跨域是指一个域下的文档或脚本想要去去请求另外一个域下的资源vue

其实一些像A连接、重定向、表单提交的资源跳转,像 <link>、<script>、<img>、<frame> 等dom标签,还有样式中 background:url()、@font-face() 等嵌入的文件外链,又好比一些像 js 发起的ajax请求、dom 和 js 对象的跨域操做等等都是跨域java

咱们一般所说的跨域,大可能是由浏览器同源策略限制引发的一类请求场景,这里你可能注意到了同源策略,那么浏览器的同源策略是什么呢?node

浏览器同源策略

同源策略/SOP(Same origin policy)是一种约定,由 Netscape 公司1995年引入浏览器,它是浏览器最核心也最基本的安全功能,若是缺乏了同源策略,浏览器很容易受到 XSS、CSFR 等攻击webpack

同源同源,什么是源呢?源指的是 协议、域名、端口 ,那么同源即三者相同,即使是不一样的域名指向同一个ip地址,也不一样源nginx

咱们来看一个域名组成,咱们以 http://www.hahaha.com/abc/a.js 为例git

  • http:// --> 协议
  • www --> 子域名
  • hahaha.com --> 主域名
  • 80 --> 端口(http:// 默认端口是80)
  • abc/a.js --> 请求资源路径

那么咱们以这个域名的源为例,来与下面这些作下对比

URL 结果 缘由
http://www.hahaha.com/abc/b.js 同源 只有路径不一样
http://www.hahaha.com/def/b.js 同源 只有路径不一样
https://www.hahaha.com/abc/a.js 不一样源 协议不一样
http://www.hahaha.com:8081/abc/a.js 不一样源 端口不一样
http://aaa.hahaha.com/abc/a.js 不一样源 主机不一样

而在不一样源的状况下,同源策略限制了咱们

  • Cookie、LocalStorage、IndexedDB 等存储性内容没法读取
  • DOM 节点和 Js对象没法得到
  • AJAX 请求发送后,结果被浏览器拦截(注意是 请求发送出去了,也拿到结果了,只是被浏览器截胡了

到了这里,相信你对跨域已经有所了解了,那么咱们如何有效的规避跨域呢,应该说如何解决跨域问题,由于咱们在开发过程当中免不了要跨域,针对不一样的类型,解决跨域的方式也有不少

不一样类型的跨域解决方案

No.1 document.domain+iframe跨域

简介

document.domain 的方式实现跨域,适用场景仅在 主域名相同,子级域名不一样 的状况下

例如,下面这两个页面

http://aaa.hahaha.com/a.html
http://bbb.hahaha.com/b.html
复制代码

那么它能够作到什么呢

  • 两个页面设置相同的 document.domain ,共享Cookie
  • 两个页面设置相同的 document.domain ,经过 iframe 实现两个页面的数据互通

示例

共享Cookie

首先,两个页面都设置相同的 document.domain

document.domain = 'hahaha.com';
复制代码

页面 a 经过脚本设置一个 Cookie

document.cookie = "test=a";
复制代码

网页 b 读这个 Cookie

let cookieA = document.cookie;
console.log(cookieA)
复制代码

服务器也能够在设置Cookie的时候,指定Cookie的所属域名为一级域名,好比.hahaha.com

Set-Cookie: key=value; domain=.hahaha.com; path=/
复制代码

这样的话,二级域名和三级域名不用作任何设置,均可以读取这个Cookie

共享数据
<!--a页面-->
<iframe src="http://bbb.hahaha.com/b.html" onload="load()" id="frame"></iframe>
<script> document.domain = 'hahaha.com'; let a = "this is a"; // 获取b页面数据 function load(){ let frame = document.getElementById("frame") console.log(frame.contentWindow.b) // this is b } </script>
复制代码
<!--b页面-->
<script> document.domain = 'hahaha.com'; let b = "this is b" // 获取a页面数据 console.log(window.parent.a); // this is a </script>
复制代码

局限

  • 首先,仅在主域名相同,子级域名不一样的状况下
  • 只适用于 Cookie 和 iframe 窗口,LocalStorage 和 IndexDB 数据没法经过这种方法共享

No.2 location.hash + iframe跨域

简介

两个页面不一样源,是没法拿到对方DOM的,典型的例子就是 iframe 窗口和 window.open 方法打开的窗口,它们与父窗口是没法通讯的

好比,不一样源的页面a和页面b,若是咱们直接获取对方数据

页面a:http://www.hahaha0.com/a.html

<iframe src="http://www.hahaha1.com/b.html" onload="load()" id="frame"></iframe>
<script> let a = "this is a" // 获取b页面数据 function load(){ console.log(document.getElementById("frame").contentWindow.b) // Uncaught DOMException: Blocked a frame from accessing a cross-origin frame. } </script>
复制代码

页面b:http://www.hahaha1.com/b.html

<!--b-->
<script> let b = "this is b" // 获取a页面数据 console.log(window.parent.a); // 报错 </script>
复制代码

显而易见,都是获取不到的,由于都跨域了,上面咱们讲到的 document.domain,只能在同主域名的状况下使用才能规避同源政策,而在主域名不相同的状况下是没有办法作到的

咱们来了解另外一种办法 window.location.hash,它拿到的是 URL 的#号后面的部分,它叫片断标识符(fragment identifier)

好比 http://hahaha.com/a.html#fragment#fragment ,若是只是改变片断标识符,页面是不会从新刷新的,就像大名鼎鼎的Vue中的hash路由就是用的这种方式

经过 location.hash + iframe 咱们能够作到在不一样主域下也能够拿到对方的数据

示例

首先,咱们要实现页面a和页面b的跨域相互通讯,由于不一样域因此利用 iframe 加上 location.hash 传值,可是这个传值是单向的,只能由一方向另外一方传值,不一样域时子页面并不能获取到父页面,也就不能相互通讯,因此咱们须要一个中间人页面c来帮忙

不一样域之间利用 iframelocation.hash 传值,相同域之间直接 JS 访问来通讯

那么咱们的逻辑就变成了下面这样

a 与 b 不一样域只能经过hash值单向通讯,b 与 c 也不一样域也只能单向通讯,但 c 与 a 同域,因此 c 可经过parent.parent 访问 a 页面全部对象

页面a:http://www.hahaha0.com/a.html

<!--a中经过iframe引入了b-->
<iframe id="frame" src="http://www.hahaha1.com/b.html"></iframe>
<script> let frame = document.getElementById('frame'); // 向b传hash值 frame.src = frame.src + '#a=我是a'; // 给同域c使用的回调方法 function cb(data) { console.log(data) // 打印 我是a+b } </script>
复制代码

页面b:http://www.hahaha1.com/b.html

<!--b中经过iframe引入了中间人c-->
<iframe id="frame" src="http://www.hahaha0.com/c.html"></iframe>
<script> let frame = document.getElementById('frame'); // 监听a传来的hash值,传给c.html window.onhashchange = function () { frame.src = frame.src + location.hash + '+b'; }; </script>
复制代码

页面c:http://www.hahaha0.com/c.html

<script> // 监听 b 的hash值变化 window.onhashchange = function () { // c调用父亲的父亲,来操做同域a的js回调,将结果传回 window.parent.parent.cb(location.hash.replace('#a=', '')); }; </script>
复制代码

No.3 window.name + iframe跨域

简介

window 对象有一个 name 属性,该属性有一个特征,即在一个窗口的生命周期内,窗口载入全部的页面都是共享一个 window.name 的,每个页面对 window.name 都有读写的权限

window.name 是持久的存在于一个窗口载入的全部页面中的,并不会由于新的页面的载入而被重置,好比下例

页面a

<script> window.name = '我是a'; setInterval(function(){ window.location = 'b.html'; // 两秒后把一个新页面b.html载入到当前的window中 },2000) </script>
复制代码

页面b

<script> console.log(window.name); // 我是a </script>
复制代码

经过上面这个例子,咱们能够很直观的看到,a 页面载入2s后,跳转到 b 页面,b 会在控制台输出 我是a

不过 window.name 的值只能是字符串的形式,最大容许2M左右,具体取决于不一样的浏览器,可是通常是够用了

那么咱们就能够利用它这一特性来实现跨域,看标题就知道是使用 window.nameiframe ,那么你能想到要如何投机取巧,哦不,是巧妙的规避跨域而不留痕迹吗?

经历过上文的摧残咱们知道,不一样域状况下的 a 页面和 b 页面,使用 iframe 嵌入一个页面,数据也是互通不了的,由于会跨域,这里咱们要使用 window.name + iframe 来实现跨域数据互通,显然咱们不能直接在 a 页面中经过改变 window.location 来载入b 页面,由于咱们如今须要实现的是 a 页面不跳转,可是也可以获取到 b 中的数据

究竟要怎么实现呢?其实仍是要靠一个中间人页面 c

首先中间人 c 要和 a 是同域

a 页面中经过 iframe 加载了 b ,在 b 页面中把数据留在了当前 iframe 窗口的 window.name 属性里

这个时候 a 是读取不了 iframe 的,由于不一样域,可是咱们能够在 a 中动态的把 iframesrc 改成 c

中间人 c 什么都不用写,由于它直接继承了 b 留下的 window.name

由于c 和 a由于是同域,因此 a 能够正常拿到子页面 c 中的 window.name 属性值

不得不说,这种作法还真挺让人叹为观止的,致敬前辈们

示例

页面a:http://www.hahaha1.com/abc/a.html

<iframe src="http://www.hahaha2.com/abc/b.html" id="frame" onload="load()"></iframe>
<script> let flag = true // onload事件会触发2次 // 第1次onload跨域页b成功后,留下数据window.name,后切换到同域代理页面 // 第2次onload同域页c成功后,读取同域window.name中数据 function load() { if(flag){ // 第1次 let frame = document.getElementById('frame') frame.src = 'http://www.hahaha1.com/abc/c.html' flag = false }else{ // 第二次 console.log(frame.contentWindow.name) // 我是b } } </script>
复制代码

页面b:http://www.hahaha2.com/abc/b.html

<script> window.name = '我是b' </script>
复制代码

No.4 window.postMessage跨域

简介

咱们上面说的几种窗口跨域作法是能够适用相应场景且安全可靠的,可是它们都是属于投机取巧,不对,是另辟捷径,可是HTML5 XMLHttpRequest Level 2中为了解决这个问题,引入了一个全新的API:跨文档通讯 API(Cross-document messaging)

这个API为 window 对象新增了一个 window.postMessage 方法,能够容许来自不一样源的脚本采用异步方式进行有限的通讯,能够实现跨文本档、多窗口、跨域消息传递

主流浏览器的兼容状况也很是可观

咱们来看下它的使用,先来看看它怎么发送数据

otherWindow.postMessage(message, targetOrigin, [transfer]);
复制代码
  • otherWindow
    • 窗口的一个引用,好比 iframecontentWindow 属性,执行 window.open 返回的窗口对象,或者是命名过的或数值索引的 window.frames
  • message
    • 要发送到其余窗口的数据,它将会被 结构化克隆算法 序列化,这意味着你能够不受什么限制的将数据对象安全的传送给目标窗口而无需本身序列化
  • targetOrigin
    • 经过窗口的 origin 属性来指定哪些窗口能接收到消息事件,指定后只有对应 origin 下的窗口才能够接收到消息,设置为通配符 * 表示能够发送到任何窗口,但一般处于安全性考虑不建议这么作,若是想要发送到与当前窗口同源的窗口,可设置为 /
  • transfer | 可选属性
    • 是一串和 message 同时传递的 Transferable 对象,这些对象的全部权将被转移给消息的接收方,而发送一方将再也不保有全部权

它也能够监听 message 事件的发生来接收数据

window.addEventListener("message", receiveMessage, false)
function receiveMessage(event) {
  let origin= event.origin
  console.log(event)
}
复制代码

接下来咱们实战下跨域状况下,经过 window.postMessage 来互通数据

示例

仍是以不一样域的页面 a 和 b 为例子

页面a:http://www.hahaha1.com/abc/a.html,建立跨域 iframe 并发送信息

<iframe src="http://www.hahaha2.com/abc/b.html" id="frame" onload="load()"></iframe>
<script> function load() { let frame = document.getElementById('frame') // 发送 frame.contentWindow.postMessage('哈喽,我是a', 'http://www.hahaha2.com/abc/b.html') // 接收 window.onmessage = function(e) { console.log(e.data) // 你好,我是b } } </script>
复制代码

页面b:http://www.hahaha2.com/abc/b.html,接收数据并返回信息

<script> // 接收 window.onmessage = function(e) { console.log(e.data) // 哈喽,我是a // 返回数据 e.source.postMessage('你好,我是b', e.origin) } </script>
复制代码

No.5 JSONP跨域

写在前面

对于 JSONP 这块,虽然不经常使用,咱们好好的提一下,由于遇到过一些初学者,把 AJAXJSONP 混为一谈了,提起 JSONP ,会说很 easy,就是在 AJAX 请求里设置一下字段就好了,可能你用过 JQuery 封装后的 JSONP 跨域方式,确实只是在请求里加个字段,可是,那是 JQ 封装好的一种使用方式而已,可不能被表象迷惑,你真的懂它的原理吗(JQ:我可不背锅!!!)

AJAX工做原理

Ajax 的原理简单来讲经过浏览器的 javascript 对象 XMLHttpRequest (Ajax引擎)对象向服务器发送异步请求并接收服务器的响应数据,而后用 javascript 来操做 DOM 而更新页面

这其中最关键的一步就是从服务器得到请求数据,即用户的请求间接经过 Ajax 引擎发出而不是经过浏览器直接发出,同时 Ajax 引擎也接收服务器返回响应的数据,因此不会致使浏览器上的页面所有刷新

使用方式也很简单

一:建立XMLHttpRequest对象,也就是建立一个异步调用对象

二:建立一个新的HTTP请求,并指定该HTTP请求的方法、URL及验证信息

三:设置响应HTTP请求状态变化的函数

四:发送HTTP请求

五:获取异步调用返回的数据
复制代码

JSONP,JSON?

JSON(JavaScript Object Notation) 你们应该是很了解,就是一种轻量级的数据交换格式,不了解的同窗能够去json.org 上了解下,分分钟搞定

JSONP(JSON with Padding) ,它是一个 非官方 的协议,它容许在服务器端集成 Script tags 返回至客户端,经过 javascript callback 的形式实现跨域访问,这就是简单的JSONP实现形式,这么说可能不太明白,那咱们来看下它究竟是怎么个原理

JSONP工做原理

先来看个小例子,仍是不一样域的 a 和 b 两页面

页面a:http://www.hahaha1.com/abc/a.html

<html>
<head>
    <title>test</title>
    <script type="text/javascript" src="http://www.hahaha2.com/abc/b.html"></script>
</head>
<body>
  <script> console.log(b) // 我是b </script>
</body>
</html>
复制代码

页面b:http://www.hahaha2.com/abc/b.js

var b = "我是b"
复制代码

能够看到,虽然不一样域,可是 a 页面中仍是能够访问到并打印出了 b 页面中的变量

这个小例子咱们能够很直观的看到 <script> 标签的 src 属性并不被同源策略所约束,因此能够获取任何服务器上脚本并执行它,这就是 JSONP 最核心的原理了,至于它如何传递数据,咱们来简单实现一个

JSONP的CallBack实现

刚才的例子说了跨域的原理,并且咱们以前有讲到 javascript callback 的形式实现跨域访问,那咱们就来修改下代码,如何实现 JSONPjavascript callback 的形式

页面a:http://www.hahaha1.com/abc/a.html

<script type="text/javascript"> //回调函数 function cb(res) { console.log(res.data.b) // 我是b } </script>
<script type="text/javascript" src="http://www.hahaha2.com/abc/b.js"></script>
复制代码

页面b:http://www.hahaha2.com/abc/b.js

var b = "我是b"

// 调用cb函数,并以json数据形式做为参数传递
cb({
  code:200, 
  msg:"success",
  data:{
    b: b
  }
})
复制代码

建立一个回调函数,而后在远程服务上调用这个函数而且将JSON 数据形式做为参数传递,完成回调,就是 JSONP 的简单实现模式,或者说是 JSONP 的原型,是否是很简单呢

JSON 数据填充进回调函数,如今懂为何 JSONPJSON with Padding 了吧

上面这种实现很简单,一般状况下,咱们但愿这个 script 标签可以动态的调用,而不是像上面由于固定在 HTML 里面加载时直接执行了,很不灵活,咱们能够经过 javascript 动态的建立 script 标签,这样咱们就能够灵活调用远程服务了,那么咱们简单改造下页面 a 以下

<script type="text/javascript"> function cb(res) { console.log(res.data.b) // 我是b } // 动态添加 <script> 标签方法 function addScriptTag(src){ let script = document.createElement('script') script.setAttribute("type","text/javascript") script.src = src document.body.appendChild(script) } window.onload = function(){ addScriptTag("http://www.hahaha2.com/abc/b.js") } </script>
复制代码

如上所示,只是些基础操做,就不解释了,如今咱们就能够优雅的控制执行了,再想调用一个远程服务的话,只要添加 addScriptTag 方法,传入远程服务的 src 值就能够

接下来咱们就能够愉快的进行一次真正意义上的 JSONP 服务调取了

咱们使用 jsonplaceholdertodos 接口做为示例,接口地址以下

https://jsonplaceholder.typicode.com/todos?callback=?
复制代码

callback=? 这个拼在接口后面表示回调函数的名称,也就是将你本身在客户端定义的回调函数的函数名传送给服务端,服务端则会返回以你定义的回调函数名的方法,将获取的 JSON 数据传入这个方法完成回调,咱们的回调函数名字叫 cb,那么完整的接口地址就以下

https://jsonplaceholder.typicode.com/todos?callback=cb
复制代码

那么话很少说,咱们来试下

<script type="text/javascript"> function cb(res) { console.log(res) } function addScriptTag(src){ let script = document.createElement('script') script.setAttribute("type","text/javascript") script.src = src document.body.appendChild(script) } window.onload = function(){ addScriptTag("https://jsonplaceholder.typicode.com/todos?callback=cb") } </script>
复制代码

能够看到,页面在加载完成后,输出了接口返回的数据,这个时候咱们再来看 JQ 中的 JSONP 实现

JSONP的JQuery实现

仍是用上面的接口,咱们来看 JQ 怎么拿数据

$.ajax({
  url:"https://jsonplaceholder.typicode.com/todos?callback=?",   
  dataType:"jsonp",
  jsonpCallback:"cb",
  success: function(res){
    console.log(res)
  }
});
复制代码

能够看到,为了让 JQ 按照 JSONP 的方式访问,dataType 字段设置为 jsonpjsonpCallback 属性的做用就是自定义咱们的回调方法名,其实内部和咱们上面写的差很少

JSONP和AJAX对比

  • 调用方式上

    • AJAXJSONP 很像,都是请求url,而后把服务器返回的数据进行处理
    • 因此类 JQuery 的库只是把 JSONP 做为 AJAX 请求的一种形式进行封装,不要搞混
  • 核心原理上

    • AJAX 的核心是经过 xmlHttpRequest 获取非本页内容
    • JSONP的核心是动态添加 script 标签调用服务器提供的 JS 脚本,后缀 .json
  • 二者区别上,

    • AJAX 不一样域会报跨域错误,不过也能够经过服务端代理、CORS 等方式跨域,而 JSONP 没有这个限制,同域不一样域均可以
    • JSONP 是一种方式或者说非强制性的协议,AJAX 也不必定非要用 json 格式来传递数据 
    • JSONP 只支持 GET 请求,AJAX 支持 GETPOST

最后,JSONP是很老的一种跨域方式了,如今基本没什么人用,因此,咱们了解懂它便可

通常状况下,咱们但愿这个script标签可以动态的调用,而不是像上面由于固定在html里面因此没等页面显示就执行了,很不灵活。咱们能够经过javascript动态的建立script标签,这样咱们就能够灵活调用远程服务了

No.6 CORS跨域资源共享

什么是CORS?

在出现 CORS 以前,咱们都是使用 JSONP 的方式实现跨域,可是这种方式仅限于 GET 请求,而 CORS 的出现,为咱们很好的解决了这个问题,这也是它成为一个趋势的缘由

CORS 是一个W3C标准,全称是 跨域资源共享(Cross-origin resource sharing)

它容许浏览器向跨源服务器,发出 XMLHttpRequest 请求,从而克服了 AJAX 只能同源使用的限制

CORS 须要浏览器和服务器同时支持,目前基本全部浏览器都支持该功能,IE浏览器不低于 IE10 便可

整个 CORS 通讯过程,都是浏览器自动完成,是不须要用户参与的,对于咱们开发者来讲,CORS 通讯与同源的 AJAX 通讯没有差异,代码彻底同样,浏览器一旦发现 AJAX 请求跨源,就会自动添加一些附加的头信息,有的时候还会多出一次附加的请求,但这个过程当中用户是无感的

所以,实现 CORS 通讯的关键是服务器,只要服务器设置了容许的 CORS 接口,就能够进行跨源通讯,要了解怎么实现 CORS 跨域通讯,咱们还要先了解浏览器对每一个请求都作了什么

浏览器会将 CORS 请求分红两类,简单请求(simple request)和非简单请求(not-so-simple request),浏览器对这两种请求的处理,是不同的

简单请求

什么是简单请求,其实很好理解记住两条就行了

  • 请求方法是 HEAD、GET、POST 三种方法之一
  • HTTP的头信息不超出如下几种字段
    • Accept
    • Accept-Language
    • Content-Language
    • Last-Event-ID
    • Content-Type(只限于三个值application/x-www-form-urlencodedmultipart/form-datatext/plain

只要同时知足这两个条件,那么这个请求就是一个简单请求

对于简单请求来讲,浏览器会直接发出CORS请求,就是在这个请求的头信息中,自动添加一个 Origin 字段来讲明本次请求的来源(协议 + 域名 + 端口),然后服务器会根据这个值,决定是否赞成此次请求

非简单请求

知道了简单请求的定义,非简单请求就比较简单了,由于只要不是简单请求,它就是非简单请求

浏览器应对非简单请求,会在正式通讯以前,作一次查询请求,叫预检请求(preflight),也叫 OPTIONS 请求,由于它使用的请求方式是 OPTIONS ,这个请求是用来询问的

浏览器会先询问服务器,当前网页所在的域名是否在服务器的许可名单之中,以及可使用哪些HTTP动词和头信息字段,只有获得确定答复,浏览器才会发出正式的 XMLHttpRequest 请求,不然就会报跨域错误

在这个预检请求里,头信息除了有代表来源的 Origin 字段外,还会有一个 Access-Control-Request-Method 字段和 Access-Control-Request-Headers 字段,它们分别代表了该浏览器 CORS 请求用到的 HTTP 请求方法和指定浏览器 CORS 请求会额外发送的头信息字段,若是你看的云里雾里,不要着急,咱们看个例子

以下为一个 AJAX 请求示例

let url = 'http://www.hahaha.com/abc'
let xhr = new XMLHttpRequest()
xhr.open('POST', url, true)
xhr.setRequestHeader('X-Token', 'YGJHJHGJAHSGJDHGSJGJHDGSJHS')
xhr.setRequestHeader('X-Test', 'YGJHJHGJAHSGJDHGSJGJHDGSJHS')
xhr.send()
复制代码

这个例子中,咱们发送了一个POST请求,并在它的请求头中添加了一个自定义的 X-TokenX-Test 字段,由于添加了自定义请求头字段,因此它是一个非简单请求

那么这个非简单请求在预检请求头信息中就会携带如下信息

// 来源
Origin: http://www.hahaha.com
// 该CORS请求的请求方法
Access-Control-Request-Method: POST
// 额外发出的头信息字段
Access-Control-Request-Headers: X-Token, X-Test
复制代码

withCredentials属性

CORS 请求默认不发送 Cookie 和 HTTP 认证信息

若是要把 Cookie 发到服务端,首先要服务端赞成,指定Access-Control-Allow-Credentials 字段

Access-Control-Allow-Credentials: true
复制代码

其次,客户端必须在发起的请求中打开 withCredentials 属性

xhr = new XMLHttpRequest()
xhr.withCredentials = true
复制代码

否则的话,服务端和客户端有一个没设置,就不会发送或处理Cookie

虽然说浏览器默认不发送 Cookie 和 HTTP 认证信息,可是有的浏览器,仍是会一块儿发送Cookie,这时你也能够显式关闭 withCredentials

xhr.withCredentials = false
复制代码

注意,如要发送 CookieAccess-Control-Allow-Origin 字段就不能设为星号,必须指定明确的、与请求网页一致的域名,同时,Cookie 依然遵循同源政策,只有用服务器域名设置的 Cookie 才会上传,其余域名的 Cookie 并不会上传,且(跨源)原网页代码中的 document.cookie 也没法读取服务器域名下的 Cookie ,下面还会提到

服务端CORS跨域配置

上面的东西只是为了让咱们理解CORS,可是要解决它仍是须要服务端配置的,不一样语言的配置项语法上可能有差别,可是内容确定都是同样的

配置容许跨域的来源

Access-Control-Allow-Origin: *
复制代码

CORS 跨域请求中,最关键的就是 Access-Control-Allow-Origin 字段,是必需项,它表示服务端容许跨域访问的地址来源,你能够写入须要跨域的域名,也能够设为星号,表示赞成任意跨源请求

注意,将此字段设置为 * 是很不安全的,建议指定来源,而且设置为 * 号后,游览器将不会发送 Cookie,即便你的 XHR 设置了 withCredentials,也不会发送 Cookie

配置容许跨域请求的方法

Access-Control-Allow-Methods: GET, POST, OPTIONS, PUT...
复制代码

该字段也是必需项,它的值是逗号分隔的一个字符串,代表服务器支持的全部跨域请求的方法

配置容许的请求头字段

Access-Control-Allow-Headers: x-requested-with,content-type...
复制代码

若是你的请求中有自定义的请求头字段,那么此项也是必须的,它也是一个逗号分隔的字符串,代表服务器支持的全部头信息字段,不限于浏览器在预检中请求的字段

配置是否容许发送Cookie

Access-Control-Allow-Credentials: true
复制代码

该字段可选,它的值是一个布尔值,表示是否容许发送Cookie,默认状况下,Cookie不包括在CORS请求之中

设为true,即表示服务器明确许可,Cookie能够包含在请求中,一块儿发给服务器

该字段只能设为true,若是服务器不要浏览器发送Cookie,删除该字段便可

配置本次预检请求的有效期

Access-Control-Max-Age: 1728000
复制代码

该字段可选,用来指定本次预检请求的有效期,单位为秒,上面结果中,有效期是20天(1728000秒),即容许缓存该条回应20天,在此期间若是你再次发出了这个接口请求,就不用发预检请求了,节省服务端资源

常见的跨域预检请求抛错

对于咱们开发时,在跨域中最容易碰钉子的地方就是预检请求,因此列举几个预检请求错误的缘由,知道哪错了能够直接找后端同窗理论,关于预检请求,最终目的只有一个,客户端发送预检,服务端容许并返回200便可

OPTIONS 404

No 'Access-Control-Allow-Origin' header is present on the requested resource
且 The response had HTTP status code 404
复制代码

服务端没有设置容许 OPTIONS 请求,那么在发起该预检请求时响应状态码会是404,由于没法找到对应接口地址

那么你可能须要找到后端,优雅的告诉他,请容许下 OPTIONS 请求

OPTIONS 405

No 'Access-Control-Allow-Origin' header is present on the requested resource
且 The response had HTTP status code 405
复制代码

服务端已经容许了 OPTIONS 请求,可是一些配置文件中(如安全配置)阻止了 OPTIONS 请求

那么你可能须要找到后端,优雅的告诉他,请关闭对应的安全配置

OPTIONS 200

No 'Access-Control-Allow-Origin' header is present on the requested resource
且 OPTIONS 请求 status 为 200
复制代码

服务器端容许了 OPTIONS 请求,配置文件中也没有阻止,可是头部匹配时出现不匹配现象

所谓头部匹配,就好比 Origin 头部检查不匹配,或者少了一些头部的支持(如 X-Requested-With 等),而后服务端就会将 Response 返回给前端,前端检测到这个后就触发 XHR.onerror ,从而致使报错

那么你可能须要找到后端,优雅的告诉他,请增长对应的头部支持

OPTIONS 500

这个就更简单了,服务端针对 OPTIONS 请求的代码出了问题,或者没有响应

那么你可能须要找到后端,将 Network 中的错误信息截一图发给他,优雅的告诉他,检测到预检请求时,请把它搞成200

No.7 Nginx代理跨域

iconfont跨域解决

浏览器跨域访问 js/css/img 等常规静态资源时被同源策略许可的,但 iconfont 字体文件好比 eot|otf|ttf|woff|svg 例外,此时可在 Nginx 的静态资源服务器中加入如下配置来解决

location / {
  add_header Access-Control-Allow-Origin *;
}
复制代码

反向代理接口跨域

咱们知道同源策略只是 浏览器 的安全策略,不是 HTTP 协议的一部分, 服务器端调用 HTTP 接口只是使用 HTTP 协议,不会执行 JS 脚本,不须要同源策略,也就不存在跨越问题

通俗点说就是客户端浏览器发起一个请求会存在跨域问题,可是服务端向另外一个服务端发起请求并没有跨域,由于跨域问题归根结底源于同源策略,而同源策略只存在于浏览器

那么咱们是否是能够经过 Nginx 配置一个代理服务器,反向代理访问跨域的接口,而且咱们还能够修改 Cookiedomain 信息,方便当前域 Cookie 写入

Nginx 其实就是各类配置,简单易学,就算没接触过,也很好理解,咱们来看示例

首先假如咱们的页面 a 在 http://www.hahaha.com 域下,可是咱们的接口却在 http://www.hahaha1.com:9999 域下

接着咱们在页面 a 发起一个 AJAX 请求时,就会跨域,那么咱们就能够经过 Nginx 配置一个代理服务器,域名和页面 a 相同,都是 http://www.hahaha.com ,用它来充当一个跳板的角色,反向代理访问 http://www.hahaha1.com 接口

# Nginx代理服务器
server {
  listen       80;
  server_name  www.hahaha.com;

  location / {
    # 反向代理地址
    proxy_pass   http://www.hahaha1.com:9999;  
    # 修改Cookie中域名
    proxy_cookie_domain www.hahaha1.com www.hahaha.com; 
    index  index.html index.htm;
		
    # 前端跨域携带了Cookie,因此Allow-Origin配置不可为*
    add_header Access-Control-Allow-Origin http://www.hahaha.com;  
    add_header Access-Control-Allow-Credentials true;
  }
}
复制代码

没错,这个代理配置相信没接触过 Nginx 也能看明白,大部分都是咱们上文提到过的,是否是很简单呢

No.8 Node代理跨域

Node 实现跨域代理,与 Nginx 道理相同,都是启一个代理服务器,就像咱们经常使用的 Vue-CLI 配置跨域,其实也是 Node 启了一个代理服务,接下来咱们来看看是如何作的

Vue-CLI中代理的多种配置

Vue-CLI 是基于 webpack 的,经过 webpack-dev-server 在本地启动脚手架,也就是在本地启动了一个 Node 服务,来实时监听和打包编译静态资源,因为都是封装好的,只须要配置便可,咱们在 vue.config.js 中配置代理以下,写法不少,列几个常见的自行选择

使用一

module.exports = {
  //...
  devServer: {
    proxy: {
      '/api': 'http://www.hahaha.com'
    }
  }
}
复制代码

如上所示时,当你请求 /api/abc 接口时就会被代理到 http://www.hahaha.com/api/abc

使用二

固然,你可能想将多个路径代理到同一个 target 下,那你可使用下面这种方式

module.exports = {
  //...
  devServer: {
    proxy: [{
      context: ['/api1', '/api2', '/api3'],
      target: 'http://www.hahaha.com',
    }]
  }
}
复制代码

使用三

正如咱们第一种使用方式代理时,代理了 /api ,最终的代理结果是 http://www.hahaha.com/api/abc ,可是有时咱们并不想代理时传递 /api,那么就可使用下面这种方式,经过 pathRewrite 属性来进行路径重写

module.exports = {
  //...
  devServer: {
    proxy: {
      '/api': {
        target: 'http://www.hahaha.com',
        pathRewrite: {'^/api' : ''}
      }
    }
  }
}
复制代码

这个时候,/api/abc 接口就会被代理到 http://www.hahaha.com/abc

使用四

默认状况下,咱们代理是不接受运行在 HTTPS 上,且使用了无效证书的后端服务器的

若是你想要接受,须要设置 secure: false ,以下

module.exports = {
  //...
  devServer: {
    proxy: {
      '/api': {
        target: 'https://www.hahaha.com',
        secure: false
      }
    }
  }
}
复制代码

使用五

配置一个字段 changeOrigin ,当它为 true 时,本地就会虚拟一个服务器接收你的请求而且代你发送该请求,因此若是你要代理跨域,这个字段是必选项

module.exports = {
  // ...
  devServer: {
    proxy: {
      "/api": {
        target: 'http://www.hahaha.com',
        changeOrigin: true,
      }
    }
  }
}
复制代码

使用六

若是你想配置多个不一样的代理,也简单,以下所示,能够在任意代理中设置对应的代理规则

module.exports = {
  // ...
  devServer: {
    proxy: {
      "/api1": {
        target: 'http://www.hahaha1.com',
        changeOrigin: true
      },
      "/api2": {
        target: 'http://www.hahaha2.com',
        pathRewrite: {'^/api2' : ''}
      },
      "/api3": {
        target: 'http://www.hahaha3.com',
        changeOrigin: true,
        pathRewrite: {'^/api3' : ''}
      }
      // ...
    }
  }
}
复制代码

注意,在本地配置代理跨域,只是解决开发时的跨域问题,当你的项目上线时,前端静态文件和后端在一个域下没有问题,若是并不在一个域下,依然会报跨域错误,这个时候还得须要后端配置跨域

Node实现代理服务器

这里咱们使用 express + http-proxy-middleware 来搭建一个代理服务器,使用 http-proxy-middleware 这个中间件没有别的意思,只是由于 webpack-dev-server 里就是使用的它

let express = require('express')
let proxy = require('http-proxy-middleware')
let app = express()

app.use('/', proxy({
    // 代理跨域目标接口
    target: 'http://www.hahaha1.com:9999',
    changeOrigin: true,

    // 修改响应头信息,实现跨域并容许带cookie
    onProxyRes: function(proxyRes, req, res) {
        res.header('Access-Control-Allow-Origin', 'http://www.hahaha.com')
        res.header('Access-Control-Allow-Credentials', 'true')
    },

    // 修改响应信息中的cookie域名,为false时,表示不修改
    cookieDomainRewrite: 'www.hahaha.com'
}))

app.listen(3000)
复制代码

No.9 WebSocket跨域

WebSocket简介

WebSocket 是一种在单个 TCP 链接上进行全双工通讯的协议,2008年诞生,2011年被 IETF 定为标准 RFC 6455,并由 RFC7936 补充规范,WebSocket API 也被 W3C 定为标准

WebSocket 使得客户端和服务器之间的数据交换变得更加简单,容许服务端主动向客户端推送数据, 在 WebSocket API 中,浏览器和服务器只须要完成一次握手,二者之间就直接能够建立持久性的链接,并进行双向数据传输,同时,它也是跨域的一种解决方案

WebSocket特色

  • 创建在 TCP 协议之上,服务器端的实现比较容易

  • 与 HTTP 协议有着良好的兼容性,默认端口也是 80 和 443,而且握手阶段采用 HTTP 协议,所以握手时不容易屏蔽,能经过各类 HTTP 代理服务器

  • 数据格式比较轻量,性能开销小,通讯高效

  • 能够发送文本,也能够发送二进制数据

  • 没有同源限制,客户端能够与任意服务器通讯

  • 协议标识符是 ws(若是加密,则为 wss ),服务器网址就是 URL

以下

ws://www.hahaha.com:80/abc/def
复制代码

示例

每一个服务端语言对 websocket 有相应的支持,写法不一样罢了,这里咱们使用 Node 作示例

在客户端咱们能够直接使用 HTML5 的 websocket API ,服务端也可使用 nodejs-websocket 实现 websocket server ,可是不建议这样作,由于原生 WebSocket API 使用起有些复杂,在浏览器的兼容性上还不够理想,因此咱们使用 Socket.io,它很好地封装了 webSocket 接口,提供了更简单、灵活的接口,也对不支持 webSocket 的浏览器提供了向下兼容,使用 Socket.io 库实现 websocket,在发送数据时能够直接发送可序列化的对象,也能够自定义消息,利用事件字符串来区分不一样消息,整个开发过程会舒服不少

想要了解更多看官网便可 Socket.io - 传送门 ,咱们来看示例

客户端:http://www.hahaha.com/a.html

<script src="/socket.io/socket.io.js"></script>
<script> let socket = io.connect('http://www.hahaha1.com:3000') socket.on('my event', (data) => { console.log(data) // { hello: 'world' } socket.emit('my other event', { my: 'data' }) }) </script>
复制代码

服务端:http://www.hahaha1.com:3000

const app = require('express').createServer()
const io = require('socket.io')(app)

app.listen(3000)

io.on('connection', (socket) => {
  socket.emit('my event', { hello: 'world' })
  
  socket.on('my other event', (data) => {
    console.log(data) // { my: 'data' }
  })
})
复制代码

如上所示,使用了 Socket.io 以后的 websocket 链接是否是超级简单呢,跟着文档本身动手试试吧

最后

欢迎你们关注公众号「不正经的前端」,时不时发一篇文章,也没有花里胡哨的推广和广告,但愿可让你们随意点开一篇文章,均可以看到满满的干货,也能够直接加机器人好友备注「前端 | 后端 | 全栈」自动经过,加交流群,闲聊、吐槽、解决问题、交朋友均可以,固然技术为主

按照时间线贴下了总结的比较全的几个帖子,还有其余的琐碎的文章,比较多就不贴了,这些文章都写的差很少,可能之间有互相抄袭,有互相借鉴,这些都是避免不了的,此文写的时候也借鉴了这些文章,只不过我手敲了一遍例子,又用我本身的理解码下来了,为此花了1周的业余时间,内容上与下面做者写的有些许雷同,那实属无奈,能够说是知识点就那么多,你们的总结稍有不一样的地方就剩表达的语法,我也难受,还特地找了工具鉴别了下类似度,以避免被误会,毕竟我也特别反感搬运工,嗯,又是一个深夜,终于收工了,睡觉喽,哦对了,码字不易,望点赞,若有错误,望指出,谢谢支持

参考文章

浏览器同源政策及其规避方法 - 阮一峰 - 2016.04

跨域资源共享 CORS 详解 - 阮一峰 - 2016.04

前端跨域整理 - 思否 damonare - 2016.10

前端常见跨域解决方案(全)- 思否 安静de沉淀 - 2017.07

正确面对跨域,别慌 - 掘金 Neal_yang - 2017.12

九种跨域方式实现原理(完整版)- 掘金 浪里行舟 - 2019.01

9种常见的前端跨域解决方案(详解)- 掘金 小铭子 - 2019.07

相关文章
相关标签/搜索