2

由于浏览器的同源策略保护机制,浏览器不能执行来自其他来源的脚本。通过 js 在不同的域之间进行数据传输或通信,比如用 ajax 向一个不同的域请求数据,或者通过 js 获取页面中不同域的框架中(iframe)的数据的操作就叫跨域

所谓同源,就是指协议、域名(IP)、端口三者都相同。只要有其中一者不相同,都是跨域,无法进行数据的交流。例如:

clipboard.png

注意的是,localhost 与 本机的 IP 地址也属于跨域。

浏览器执行 javascript 脚本时,会检查这个脚本属于哪个页面,如果不是同源页面,就不会被执行。


解决跨域的方法

解决方法主要有以下几种:

方法一:设置 webpack 的服务器代理

1、在工程化的前端项目中,在本机进行开发时,后台的接口可能会出现跨域,可以设置 webpack 的服务器代理,将本机的请求转发到与接口同源的地址。

首先安装插件:webpack-dev-server,然后再 config 文件夹里找到 index.js 文件,在 dev 对象下的 proxyTable 字段添加对应的需要替换的地址。

module.exports = {

  ...

  devServer: {   //本地开发服务器设置

    ...

    port: 8080,     //当前的端口号

    proxyTable: {  //服务器代理设置

      '/v3/admin': {  //需要代理的接口形式

        target: 'http://localhost:8700',  //将当前端口号代理到8700

        changeOrigin: true,

        secure: false

      }

    }

  }

}

以上设置就是将所有包含 '/v3/admin' 字段的接口,从原来的 localhost: 8080 代理到 localhost: 8700 ,变成 'http://localhost: 8700/v3/admin' 从而获取到 8700 端口的内容。


方法二:使用 CORS 进行数据传输

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

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

CORS 标准新增了一组 HTTP 首部字段,允许服务器声明哪些源站有权限访问哪些资源。另外,规范要求,对那些可能对服务器数据产生副作用的 HTTP 请求方法(特别是 GET 以外的 HTTP 请求,或者搭配某些 MIME 类型的 POST 请求),浏览器必须首先使用 OPTIONS 方法发起一个预检请求(preflight request),从而获知服务端是否允许该跨域请求。服务器确认允许之后,才发起实际的 HTTP 请求。在预检请求的返回中,服务器端也可以通知客户端,是否需要携带身份凭证(包括 Cookies和 HTTP 认证相关数据)。

CORS需要浏览器和服务器同时支持,浏览器需要 ie8 以上

浏览器端的写法:

    function CORSRequest(method,url,opation,callback) {
    
      var xhr = new XMLHttpRequest();
    
      if ("withCredentials" in xhr) {
    
        // 此时即支持CORS的情况
    
        // 检查XMLHttpRequest对象是否有“withCredentials”属性
    
        // “withCredentials”仅存在于XMLHTTPRequest level 2对象里
    
      } else {
    
        // 否则检查是否支持XDomainRequest
    
        // XDomainRequest仅存在于IE中,是IE8 和 IE9 用于支持CORS请求的方式
    
        xhr = new XDomainRequest();
    
      }
    
      xhr.open(method, url, true);
    
      if(method=="POST"){  //可以设置不同请求方法的操作,所有的请求都可以分别设置
    
        xhr.setRequestHeader("Content-type","application/x-www-form-urlencoded");  //设置请求头
    
        xhr.send(opation);  //传输数据到服务器
    
      }else{
    
        xhr.send();
    
      }
    
      xhr.onload = function(){
    
        callback(xhr.responseText);
    
      }
    
    };
    
    function notice(data) {
    
      console.log(data)
    
    }
    
    CORSRequest('POST','http://example.com','传递给服务器的数据',notice);

服务器端的写法:

Apache:

Apache需要使用 mod_headers 模块来激活 HTTP 头的设置,它默认是激活的。你只需要在 Apache 配置文件的, , 或的配置里加入以下内容即可:

    Header set Access-Control-Allow-Origin *  

PHP:

    <?php
    
      header("Access-Control-Allow-Origin:*");  //允许所有来源访问
    
      header('Access-Control-Allow-Method:POST,GET');  //允许访问的方式

以上的配置的含义是允许任何域发起的请求都可以获取当前服务器的数据。当然,这样有很大的危险性,恶意站点可能通过XSS攻击我们的服务器。所以我们应该尽量有针对性的对限制安全的来源,例如:

    <?php
    
      header("Access-Control-Allow-Origin: http://blog.csdn.net");

CORS 的优点:

CORS与JSONP相比,无疑更为先进、方便和可靠。

  1. JSONP 只能实现 GET 请求,而 CORS 支持所有类型的HTTP请求。
  2. 使用 CORS,开发者可以使用普通的 XMLHttpRequest 发起请求和获得数据,拥有onerror 和 onabort 方法,比起 JSONP 有更好的错误处理。
  3. JSONP 主要被老的浏览器支持,它们往往不支持 CORS,而绝大多数现代浏览器都已经支持了 CORS。

注意点:

当发送的请求是非简单请求,浏览器会在正式通信之前,增加一次 HTTP 查询请求,称为"预检"请求( preflight )。浏览器先询问服务器,当前网页所在的域名是否在服务器的许可名单之中,以及可以使用哪些 HTTP 动词和头信息字段。只有得到肯定答复,浏览器才会发出正式的 XMLHttpRequest 请求,否则就报错。

一般前端框架比如"ExtJS"、"AngularJS", 框架监测到访问的域名可能存在跨域的话会先发送一个 OPTIONS 请求,验证是否可进行通信,如果返回可通信才会真正发起一个 POST、GET 请求。

下图是框架发起的 OPTIONS 请求,当如果服务器的 Nginx 并没有设置允许跨域请示的时候,它会返回一个 405 状态码。

clipboard.png

axios 框架也会发送 OPTIONS 请求。

关于 CROS 的更多内容,可以进入以下链接了解:

https://developer.mozilla.org...

http://www.ruanyifeng.com/blo...


方法三:JSONP

在 js 中,我们直接用 XMLHttpRequest 请求不同域上的数据时,是不可以的。但是,在页面上引入不同域上的 js 脚本文件却是可以的,jsonp 正是利用这个特性来实现的。

JSONP是一种非正式传输协议,该协议的一个要点就是允许用户传递一个约定的参数(一般是 callback )给服务端,然后服务端返回数据时会将这个 callback 参数作为函数名来包裹住JSON数据,这样客户端就可以随意定制自己的函数来自动处理返回数据了。

比如,有个 a.html 页面,它里面的代码需要利用 ajax 获取一个不同域上的 json 数据,假设这个 json 数据地址是 http://example.com/data.php , 那么 a.html 中的代码就可以这样:

图片描述

我们看到获取数据的地址后面还有一个 callback 参数,按惯例是用这个参数名,但是你用其他的也一样。当然如果获取数据的jsonp地址页面不是你自己能控制的,就得按照提供数据的那一方的规定格式来操作了。关键是参数值必须是当前页面 script 设置的数据处理回调函数的名字。

因为是当做一个js文件来引入的,所以http://example.com/data.php 返回的必须是一个能执行的js文件,所以这个页面的php代码可能是这样的:

clipboard.png

最终那个页面输出的结果是:

clipboard.png

所以通过 http://example.com/data.php?c... 得到的 js 文件,就是我们之前定义的 dosomething 函数,并且它的参数就是我们需要的 json 数据,这样我们就跨域获得了我们需要的数据。

这样 jsonp 的原理就很清楚了,通过 script 标签引入一个 js 文件,这个 js 文件载入成功后会执行我们在 url 参数中指定的函数,并且会把我们需要的 json 数据作为参数传入。所以 jsonp 是需要服务器端的页面进行相应的配合的。

知道 jsonp 跨域的原理后我们就可以用 js 动态生成 script 标签来进行跨域操作了,而不用特意的手动的书写那些 script 标签。

一个跨域获取淘宝关键字搜索建议的例子:

clipboard.png

效果:

clipboard.png

优点:

  • 不受同源策略的限制;
  • 兼容性更好,在更加古老的浏览器中都可以运行,不需要XMLHttpRequest或ActiveX的支持;
  • 请求完毕后可以通过调用callback的方式回传结果。

缺点:

  • 只支持GET请求而不支持POST等其它类型的HTTP请求;
  • 只支持跨域HTTP请求这种情况,不能解决不同域的两个页面之间如何进行JavaScript调用的问题。

方法四:设置代理服务器

例如 www.123.com/index.html 需要调用 www.456.com/server.php,可以写一个接口 www.123.com/server.php ,由这个接口在后端去调用 www.456.com/server.php 并拿到返回值,然后再返回给 index.html,这就是一个代理的模式。相当于绕过了浏览器端,自然就不存在跨域问题。

例如:

a.php 后台获取跨域的数据

    <?php
    
      $url="https://api.douban.com/v2/movie/subject/1764796”;  //接口地址
    
      $content=file_get_contents($url);
    
      echo $content;
    
    ?>

前端只要将 XMLHttpRequest 的请求地址设置为 'a.php' 就可以。


方法五:使用 window.name

window 对象有个 name 属性,该属性有个特征:即在一个窗口( window )的生命周期内,窗口载入的所有的页面都是共享一个 window.name 的,每个页面对 window.name 都有读写的权限,window.name 是持久存在一个窗口载入过的所有页面中的,并不会因新页面的载入而进行重置。

比如:有一个页面 a.html,它里面有这样的代码:

clipboard.png

再看看 b.html 页面的代码:

clipboard.png

a.html 页面载入后 3 秒,跳转到了 b.html 页面,结果为:

clipboard.png

我们看到在 b.html 页面上成功获取到了它的上一个页面 a.html 给 window.name 设置的值。如果在之后所有载入的页面都没对 window.name 进行修改的话,那么所有这些页面获取到的 window.name 的值都是 a.html 页面设置的那个值。当然,如果有需要,其中的任何一个页面都可以对 window.name 的值进行修改。注意,window.name 的值只能是字符串的形式,这个字符串的大小最大能允许 2M 左右甚至更大的一个容量,具体取决于不同的浏览器,但一般是够用了。

上面的例子中,我们用到的页面 a.html 和 b.html 是处于同一个域的,但是即使 a.html 与 b.html 处于不同的域中,上述结论同样是适用的,这也正是利用 window.name 进行跨域的原理。

此方法需要与 iframe 配合使用。

比如有一个 www.example.com/a.html 页面,需要通过 a.html 页面里的 js 来获取另一个位于不同域上的页面 www.cnblogs.com/data.html 里的数据。

data.html 页面里的代码很简单,就是给当前的 window.name 设置一个 a.html 页面想要得到的数据值。data.html 里的代码:

clipboard.png

那么在 a.html 页面中,我们怎么把 data.html 页面载入进来呢?显然我们不能直接在 a.html 页面中通过改变 window.location 来载入 data.html 页面,因为我们想要即使 a.html 页面不跳转也能得到 data.html 里的数据。答案就是在 a.html 页面中使用一个隐藏的 iframe 来充当一个中间人角色,由 iframe 去获取 data.html 的数据,然后 a.html 再去得到iframe获取到的数据。

充当中间人的 iframe 想要获取到 data.html 的通过 window.name 设置的数据,只需要把这个 iframe 的 src 设为 www.cnblogs.com/data.html 就行了。然后 a.html 想要得到 iframe 所获取到的数据,也就是想要得到 iframe的window.name 的值,还必须把这个 iframe 的 src 设成跟 a.html 页面同一个域才行,不然根据前面讲的同源策略,a.html 是不能访问到 iframe 里的 window.name 属性的。这就是整个跨域过程。

看下 a.html 页面的代码:

clipboard.png

上面的代码只是最简单的原理演示代码,你可以对使用 js 封装上面的过程,比如动态的创建 iframe ,动态的注册各种事件等等,当然为了安全,获取完数据后,还可以销毁作为代理的 iframe。

方法六:使用 window.postMessage

window.postMessage(message,targetOrigin) 方法是 html5 新引进的特性,可以使用它来向其它的 window 对象发送消息,无论这个 window 对象是属于同源或不同源,目前 IE8+、FireFox、Chrome、Opera 等浏览器都已经支持 window.postMessage 方法。

调用 postMessage 方法的 window 对象是指要接收消息的那一个 window 对象,该方法的第一个参数 message 为要发送的消息,类型只能为字符串;第二个参数 targetOrigin 用来限定接收消息的那个 window 对象所在的域,如果不想限定域,可以使用通配符 * 。

需要接收消息的 window对 象,可是通过监听自身的 message 事件来获取传过来的消息,消息内容储存在该事件对象的 data 属性中。

上面所说的向其他 window 对象发送消息,其实就是指一个页面有几个框架的那种情况,因为每一个框架都有一个 window 对象。下面看一个简单的示例,有两个页面

clipboard.png

clipboard.png

我们运行 a 页面后得到的结果:

clipboard.png

我们看到 b 页面成功的收到了消息。

使用 postMessage 来跨域传送数据还是比较直观和方便的,但是缺点是 IE6、IE7 不支持,所以用不用还得根据实际需要来决定。


将臣
2.3k 声望107 粉丝

学习、分享技术