前端跨域方法总结

为何须要跨域

浏览器出于安全的考虑,引入了同源策略。这种策略会对咱们页面上执行的js访问资源的时候进行限制,好比咱们不能直接经过js访问不一样源之下的页面DOM结构,同时在对不一样源发送请求时也没法获取到服务器响应内容(服务器会正常处理请求并返回响应内容,可是返回的内容被浏览器拦截掉了)。这里还牵扯到“源”这个概念,若是咱们访问的目标url和当前页面所在的url二者的协议、域名、端口只要有一个不相同,那么就认为是属于两个不一样的源。明白了源的定义以后,咱们再来看看在同源策略的做用下,咱们能够在页面上作的以及不能作的都有哪些操做。javascript

先说可以作的,好比经过js重定向咱们的页面(修改location.href),表单提交,这些都是能够的。还有就是经过嵌入一些HTML标签来加载咱们须要的资源,好比script标签引入一段脚本、img标签插入一张图片、link标签加载样式文件、iframe嵌入不一样源的页面等等也都是能够的。这些也是咱们平常开发过程当中再正常不过的操做了。html

可是,同源策略对js访问一些敏感资源则进行了限制。除了开头提到的那两点以外,还有就是js中没法访问不属于同个源的cookie、LocalStorage中存储的内容。具体来讲,cookie和LocalStorage在控制哪些源能够访问的问题上仍是细微的差异,父域在设置cookie的时候能够设定容许子域访问这段cookie,同时Cookie只和域名以及路径关联,若是是同个域名不一样端口的源依然是共享同个域名下的Cookie的,而LocalStorage则是以源为单位进行管理,相互独立,不一样源之间没法相互访问LocalStorage中的内容。前端

经常使用的跨域方法

客户端与服务端通讯

客户端与不一样源的服务器的通讯问题是日常开发过程当中须要解决的很常见的问题,主要有如下几种方式:java

CORS

这种方法应该是用得比较多的一种。CORS全称是Cross-Origin Resource Sharing,翻译过来就是跨域资源共享。基本思想就是引入一些自定义的HTTP Header来完成客户端与服务端的通讯。json

对于一些简单请求,浏览器在发送请求时会带上Origin请求头,指示当前的源,服务器端在处理请求时不会去检查当前请求来源是否合法,依然会正常处理请求并响应,最终浏览器在拿到响应以后会检查服务端响应的Access-Control-Allow-Origin列表中是否存在当前页面所在的源,若是不存在会直接block掉当前请求。canvas

在浏览器看来,同时知足如下条件的请求都认为是简单请求:后端

请求方法为GET或者POST; 只包含Accept、Accept-Language、Content-Language或者Content-Type(取值为application/x-www-form-urlencoded,multipart/form-data, 或者text/plain),其他状况的Header则属于非简单Header;api

对于非简单请求,浏览器会先向服务器发送一个Preflight请求,该请求使用Option方法,并包含如下Header:跨域

  1. Origin
  2. Access-Control-Request-Method:询问服务器是否支持某方法;
  3. Access-Control-Request-Headers:询问服务器是否支持请求中包含的非简单Header;

其中后两个Header只会出如今Preflight请求中。而后浏览器收到包含如下Header的服务器响应:浏览器

  1. Access-Control-Allow-Origin
  2. Access-Control-Allow-Methods:对客户端回应服务器支持的请求方法列表;
  3. Access-Control-Allow-Headers:对客户端回应服务器支持的Header;

Preflight请求至此也算是告一段落,以后浏览器会检查当前请求发出的源是否在服务端响应的Access-Control-Allow-Origin列出的源的列表中,若是是才会发送真正的请求。在实验过程当中,浏览器并不必定要在服务器支持Preflight请求查询的请求方法和Header时才发送真正的请求,只要发出请求的源是合法的就会在Preflight请求以后把请求发出去。

JSONP

JSONP 的全称是 JSON with Padding,译为被填充的JSON。前端在指定要请求的URL时能够经过和后端约定一个指定回调函数名称的参数,确保后台响应的脚本片断中调用了前端指定的回调函数,以此能够实现发送多个JSONP请求并且互不干扰。具体JSONP实现以下:

var delicious_callbacks = {};
function jsonp(url, callback) {
  var uid = (new Date()).getTime();
  delicious_callbacks[uid] = function (data) {
    delete delicious_callbacks[uid];
    callback(data);
  };
  url += "?jsonp=" + encodeURIComponent("delicious_callbacks[" + uid + "]");
  var script = document.createElement('script')
  script.src = url
  document.body.appendChild(script)
};

jsonp("http://example.com/api", function(data) { // here we get the data });
复制代码

JSONP这种方式自己也是存在必定缺陷的,很明显它只能用于GET请求。另外,后端应用程序在处理过程可能会出现4xx、5xx错误或者遇到其余意外状况,致使没法返回正确的js函数调用格式的字符串的状况,因此还须要监听script标签的onerror事件来处理可能出现的意外状况。

Cookie跨域共享

cookie做为客户端存储的一种方案,在客户端设置cookie也有如下几种方法:

  1. 配置服务端返回Set-Cookie响应头
  2. 在页面上的JavaScript代码中经过document.cookie,直接设置document.cookie为咱们须要存储的内容并不会覆盖现有的cookie,举例:
document.cookie="name=Jack;path=/"
document.cookie="age=25;path=/" // cookie中会同时保存name和age这两个字段
复制代码

cookie是有过时时间的,若是像上面的代码同样没有显式地设置cookie的过时时间,则在浏览器退出以后相应的cookie也会被清除。

通常状况下,浏览器在访问页面时会自动将和当前域名以及路径匹配的Cookie发送到服务器端。而在通常状况下,咱们在页面中发出的Ajax请求则不会自动将当前URL关联的Cookie同请求一同发送到服务端。若是咱们须要在请求中将和当前访问的URL的Cookie发送到服务端,能够设置XMLHttpRequest对象的withCredentials属性为true。

而若是是要将当前页面所在域名的Cookie发送到另外一个域名下的服务端,这时候须要对服务端进行配置,使其支持CORS,同时还须要注意此时服务端返回的Access-Control-Allow-Origin不能再设置为‘*’,同时服务端须要返回Access-Control-Allow-Credentials: true,不然服务端的响应依然会被浏览器block掉。

在这样配置以后,浏览器在发送这种携带凭据信息(也就是Cookie)的Ajax请求时就会把当前页面所在的域名下path属性和请求的URL相匹配(好比当前请求的URL为/test/example,那么设置在/,/test/,/test/example这些path之下的Cookie会被发送)的Cookie一同发送到服务端。这样就实现了Cookie的跨域共享。

跨页面通讯

除了和不一样源的服务器进行通讯的需求之外,咱们还会遇到跨页面通讯问题,须要访问其余页面上的一些信息,或者将一些数据持久化,以供其余页面取用。具体方式以下:

document.domain

经过这种方式跨域的两个源须要知足必定的条件的,即两个源的域名须要是父子域的关系或者是相同的域。由于页面设置document.domain的值只能是当前域自己,或者是父域,而不能是其余不相关的域名。只有两个页面的document.domain都设置成相同的值,嵌入iframe的页面和iframe加载的页面才能相互获取到彼此的页面信息(包括DOM结构、window对象等)。

在实践中也发现须要注意的两个问题:

  1. 若是两个页面所在的源是同样的,能够直接通讯,可是若是两个页面所在的域名相同但端口不一样或者是其余状况,那么两个页面仍须要设置相同的document.domain,不然仍是会被浏览器block掉。具体缘由在MDN上也有提到:

浏览器单独保存端口号。任何的赋值操做,包括document.domain = document.domain都会致使端口号被重写为null。所以company.com:8080不能仅经过设置document.domain = "company.com"来与company.com通讯。必须在他们双方中都进行赋值,以确保端口号都为null

  1. 须要在嵌入的iframe加载完成以后才能和其加载的子页面进行通讯,不然拿到的值可能仍是undefined。

window对象name属性

浏览器具备这样一个特性:同一个标签页或者同一个iframe框架加载过的页面共享相同的window.name属性值,意味着只要是在同一个标签页里面打开过的页面(不论是否同源),这些页面上window.name属性值都是相同的。利用这个特性,就能够将这个属性做为在不一样页面之间传递数据的介质。

若是是经过iframe+window.name这种方式在彻底没有父子域关系的两个源之间传递数据(假设源A要获取源B中的数据),源A页面上的iframe在加载源B的目标页面(源B页面把数据设置在window.name属性上)以后还须要再跳转到源A的某个页面上,以便于嵌入iframe的页面经过(上面介绍的)和在iframe中的页面将document.domain都设置为源A的方式来获取iframe中的数据。示例代码以下:

// www.a.com/getData.html
<script type="text/javascript"> function getData() { var frame = document.getElementsByTagName("iframe")[0]; frame.onload = function () { var data = frame.contentWindow.name; // 此处获取数据 alert(data); }; frame.contentWindow.location = "./aaa.html"; // 加载完www.b.com/data.html以后就加载www.a.com/下随便一个页面,获取数据 } </script>
<iframe src="http://www.b.com/data.html" style="display: none;" onload="getData();"></iframe>
复制代码

HTML5 cross-document message

HTML5中引入了另一种跨页面通讯的方式,称为跨文档消息传送。一样能够实现主页面和嵌入的iframe子页面(或者由当前页面打开的页面)之间完成数据的传递,另外这种方法也能够用于当前JavaScript引擎线程和其余worker线程之间完成数据交换。若是是与经过iframe加载的子页面进行通讯,则须要先获取到接收数据的目标页面的window对象(具体经过前面提到的方法来获取),经过该对象的postMessage方法能够向目标页面发送数据。

<!--send.html-->
<iframe src="./receiver.html" id="frame"></iframe>
<button id="send-btn">send message</button>
<script> var frame = document.getElementById('frame') document.getElementById('send-btn').addEventListener('click', function() { frame.contentWindow.postMessage({ name: 'Jack' }, 'http://localhost:8888') // 接收信息的页面所在的源 }) </script>

<!--receiver.html-->
<script> window.addEventListener('message', function(e) { // 验证消息发送方所在的源 if(e.origin === 'http://localhost:8888') { console.log(e.data) e.source.postMessage(...) // 回送消息 } }) </script>
复制代码

若是是须要和页面上的worker进行通讯,直接调用建立出来的Worker实例的postMessage方法,在Worker实例执行的脚本中则经过self或者this来访问Worker实例,进而调用postMessage方法来完成通讯。

localStorage

localStorage是HTML5引入的客户端存储方案,经过localStorage存储的内容会一直保存在客户端,除非调用removeItem方法显式移除,不然内容将永久保留。MDN上对localStorage的介绍也提到了一种经过cookie在不支持localStorage的浏览器上实现localStorage的方法,经过将cookie的过时时间设置为将来很长以后的一个时间点能够模拟localStorage永久保留的特性,而在模拟localStorage移除存储内容时则将对应的cookie。更进一步,若是不设置cookie的过时时间,还能够用来模拟浏览器中的另外一种客户端存储方案--sessionStorage。和cookie不一样的是,localStorage提供的存储容量上限更大。

前面也提到了,localStorage存储的内容是以源为单位进行管理的,这意味着即便域名相同,端口不一样的页面也没法经过localStorage进行通讯的。在浏览器的多个标签页中分别打开多个同源页面,这些页面中的window对象能够经过监听storage事件,当其余标签页的页面在设置localStorage中的内容时会触发该事件来进行通知,经过这种方式也能够实现跨页面通讯。

其余跨域问题

字体文件加载

CSS中引用的字体文件加载也存在跨域问题,须要设置CORS才能加载其余域下的字体文件。默认状况下定义新的字体不会当即去下载对应的字体文件,只有当页面上的元素使用了这种字体才会去下载对应的字体文件。

跨域脚本错误处理

对于页面上加载的跨域脚本执行出错,页面上绑定的错误处理函数window.onerror在默认状况下是获取不到具体的错误信息的,这时候须要在加载跨域脚本的标签上使用crossorigin属性,也就是在请求跨域脚本的时候执行CORS。crossorigin属性能够设置的值有:

  1. anonymous:请求脚本的时候不会携带凭据
  2. use-credentials:请求脚本的时候携带凭据

设置为其余值都会被看做是anonymous关键字。设置了crossorigin属性意味着还须要对服务器进行配置,使其支持CORS。若是服务端没有正确配置CORS,跨域脚本是没法正常下载的。

canvas绘制内容转化为文件对象

canvas中动态加载的图片能够直接画到canvas中,可是在将canvas转化成文件对象进行操做时也存在跨域问题,会遇到“Tainted canvases may not be exported”错误。这时候须要对动态加载的图片对象设置crossOrigin属性,同时也须要配置服务器使其支持CORS。

let img = new Image()
img.crossOrigin = 'anonymous'
img.src = "//localhost:8888/images/1751527990314_.pic.jpg"
img.onload = () => {
  let canvas = document.getElementById('canvas')
  let ctx = canvas.getContext('2d')
  ctx.drawImage(img, 0, 0)
  canvas.toBlob(blob => console.log(blob), 'image/jpeg', .75)
}
复制代码

总结

本文主要介绍了浏览器中的同源策略以及如何在同源策略的约束之下完成隶属不一样源的客户端和服务端通讯,以及跨页面通讯。这些跨域方法在实际使用中也须要从具体的场景出发,根据不一样的通讯需求采用合适的方法。以上,若有疏漏之处,还望斧正。

相关文章
相关标签/搜索