跨域解决方案汇总

同源策略

由于浏览器的同源策略,如果两个页面的协议,端口(如果有指定)和域名都相同,则两个页面具有相同的源,只要有一者不同,就会造成跨域

jsonp

jsonp由两部分组成:回调函数和数据。回调函数是当响应到来时应该在页面中调用的函数。回调函数的名字一般是在请求中指定。而数据就是传入回调函数中的JSON数据。形式如下:

<script>
var localHandle = function(data){
    alert("返回的数据:" + data);
}
</script>

<script src="http://remoteserver.com/remote.js?name=value&callback=localHandle"></script>

remote.js代码如下:

localHandler({"result":"我是远程js带来的数据"});

服务器根据请求里的callback=localHandle知道了本地的回调函数名称localHandle,查询字符串为name=value,然后服务器执行相对应的函数并返回数据给本地的localHandle回调函数。

建议动态的创建script来查询

<script>
var localHandle = function(data){
    alert("返回的数据:" + data);
}

// 提供jsonp服务的url地址(不管是什么类型的地址,最终生成的返回值都是一段javascript代码)
var url = "http://remoteserver.com/remote.js?name=value&callback=localHandle";
// 创建script标签,设置其属性
var script = document.createElement('script');
script.setAttribute('src', url);
// 把script标签加入head,此时调用开始
document.getElementsByTagName('head')[0].appendChild(script);
</script>

jsonp优点

简单易用,能够直接访问响应文本,支持浏览器和服务器之间的双向通信

jsonp缺点

  • 安全性不足。借助JSONP有可能进行跨站请求伪造(CSRF)攻击,当一个恶意网站使用访问者的浏览器向服务器发送请求并进行数据变更时,被称为CSRF攻击。由于请求会携带cookie信息,服务器会认为是用户自己想要提交表单或者发送请求,而得到用户的一些隐私数据。
  • 错误原因不易找。JSONP缺乏错误处理机制,如果脚本注入成功后,就会调用回调函数,但是注入失败后,没有任何提示。这就意味着,当JSONP遇到404、505或者其他服务器错误时,你是无法检测出错原因的。我们能够做的也只有超时,没有收到响应,便认为请求失败,执行对应的错误回调。
  • 只能适用于get请求。只能使用GET请求就意味着很多限制,提交到服务器的数据量将受限于浏览器的最大URL长度。

jsonp封装

/**
 * JSONP请求工具
 * @param url 请求的地址
 * @param data 请求的参数
 * @returns {Promise<any>}
 */
const request = ({url, data}) => {
  return new Promise((resolve, reject) => {
    // 处理传参成xx=yy&aa=bb的形式
    const handleData = (data) => {
      const keys = Object.keys(data)
      const keysLen = keys.length
      return keys.reduce((pre, cur, index) => {
        const value = data[cur]
        const flag = index !== keysLen - 1 ? '&' : ''
        return `${pre}${cur}=${value}${flag}`
      }, '')
    }
    // 动态创建script标签
    const script = document.createElement('script')
    // 接口返回的数据获取
    window.jsonpCb = (res) => {
      document.body.removeChild(script)
      delete window.jsonpCb
      resolve(res)
    }
    script.src = `${url}?${handleData(data)}&cb=jsonpCb`
    document.body.appendChild(script)
  })
}
// 使用方式
request({
  url: 'http://localhost:9871/api/jsonp',
  data: {
    // 传参
    msg: 'helloJsonp'
  }
}).then(res => {
  console.log(res)
})

空iframe加form

const requestPost = ({url, data}) => {
  // 首先创建一个用来发送数据的iframe.
  const iframe = document.createElement('iframe')
  iframe.name = 'iframePost'
  iframe.style.display = 'none'
  document.body.appendChild(iframe)
  const form = document.createElement('form')
  const node = document.createElement('input')
  // 注册iframe的load事件处理程序,如果你需要在响应返回时执行一些操作的话.
  iframe.addEventListener('load', function () {
    console.log('post success')
  })

  form.action = url
  // 在指定的iframe中执行form
  form.target = iframe.name;   //target规定在何处打开 action URL。这里可以通过iframe.name来指定iframe
  form.method = 'post'
  for (let name in data) {
    node.name = name
    node.value = data[name].toString()
    form.appendChild(node.cloneNode())
  }
  // 表单元素需要添加到主文档中.
  form.style.display = 'none'
  document.body.appendChild(form)
  form.submit()

  // 表单提交后,就可以删除这个表单,不影响下次的数据发送.
  document.body.removeChild(form)
}
// 使用方式
requestPost({
  url: 'http://localhost:9871/api/iframePost',
  data: {
    msg: 'helloIframePost'
  }
})

document.domain + iframe

通过document.domain将两个页面的域名都设置成相同域名,也可以实现跨域。 不过这个方法只适用于不同子域的框架间的交互

// a.html
<iframe id="iframe" src="http://child.domain.com/b.html"></iframe>
<script>
    document.domain = 'domain.com';
    var user = 'admin';
</script>
// b.html
<script>
    document.domain = 'domain.com';
    // 获取父窗口中变量
    alert('get js data from parent ---> ' + window.parent.user);
</script>

location.hash

通过location.hash来跨域,原理是改变url的hash部分来进行双向通信,父页面向iframe子页面通信只需监听自身的url变化来发送信息,而子页面向父页面通信就麻烦一些,由于两个页面不在同一个域,IE和Chrome都不允许修改parent.location.hash的值,所以需要创建一个和父页面同域的中间页,中间页可利用parent.parent访问父页面的所有对象。

该方法的缺点是会造成不必要的浏览器历史记录,并且有些浏览器不支持onhashchange事件,数据直接暴露在url中,数据容量和类型都有限等。

 // a.html
<iframe id="iframe" src="http://www.domain2.com/b.html" style="display:none;"></iframe>
<script>
    var iframe = document.getElementById('iframe');

    // 向b.html传hash值
    setTimeout(function() {
        iframe.src = iframe.src + '#user=admin';
    }, 1000);

    // 开放给同域c.html的回调方法
    function onCallback(res) {
        alert('data from c.html ---> ' + res);
    }
</script>
 // b.html
<iframe id="iframe" src="http://www.domain1.com/c.html" style="display:none;"></iframe>
<script>
    var iframe = document.getElementById('iframe');

    // 监听a.html传来的hash值,再传给c.html
    window.onhashchange = function () {
        iframe.src = iframe.src + location.hash;
    };
</script>
 // c.html
<script>
    // 监听b.html传来的hash值
    window.onhashchange = function () {
        // 再通过操作同域a.html的js回调,将结果传回
        window.parent.parent.onCallback('hello: ' + location.hash.replace('#user=', ''));
    };
</script>

window.name + iframe

window.name + iframe,利用window.name在不同页面(甚至不同域)加载后依旧存在,并且支持大小达到了2MB。 步骤: 首先在a页面中创建iframe,将src指向外域保存数据到window.name,再将iframe的src指向和a页面同域的b代理页面,借此让a页面以iframe.contentWindow.name的形式成功读取数据。

//a.html
var proxy = function(url, callback) {
    var state = 0;
    var iframe = document.createElement('iframe');

    // 加载跨域页面
    iframe.src = url;

    // onload事件会触发2次,第1次加载跨域页,并留存数据于window.name
    iframe.onload = function() {
        if (state === 1) {
            // 第2次onload(同域proxy页)成功后,读取同域window.name中数据
            callback(iframe.contentWindow.name);
            destoryFrame();

        } else if (state === 0) {
            // 第1次onload(跨域页)成功后,切换到同域代理页面
            iframe.contentWindow.location = 'http://www.domain1.com/proxy.html';
            state = 1;
        }
    };

    document.body.appendChild(iframe);

    // 获取数据以后销毁这个iframe,释放内存;这也保证了安全(不被其他域frame js访问)
    function destoryFrame() {
        iframe.contentWindow.document.write('');
        iframe.contentWindow.close();
        document.body.removeChild(iframe);
    }
};

// 请求跨域b页面数据
proxy('http://www.domain2.com/b.html', function(data){
    alert(data);
});
//b.html
<script>
    window.name = 'This is domain2 data!';
</script>

postMessage跨域

postMessage是HTML5新增的window属性

用法:postMessage(data,origin)方法接受两个参数

//a.html
<iframe id="iframe" src="http://www.domain2.com/b.html" style="display:none;"></iframe>
<script>
    var iframe = document.getElementById('iframe');
    iframe.onload = function() {
        var data = {
            name: 'aym'
        };
        // 向domain2传送跨域数据
        iframe.contentWindow.postMessage(JSON.stringify(data), 'http://www.domain2.com');
    };

    // 接受domain2返回数据
    window.addEventListener('message', function(e) {
        alert('data from domain2 ---> ' + e.data);
    }, false);
</script>
//b.html
<script>
    // 接收domain1的数据
    window.addEventListener('message', function(e) {
        alert('data from domain1 ---> ' + e.data);

        var data = JSON.parse(e.data);
        if (data) {
            data.number = 16;

            // 处理后再发回domain1
            window.parent.postMessage(JSON.stringify(data), 'http://www.domain1.com');
        }
    }, false);
</script>

CORS

CORS需要浏览器和服务器同时支持。目前,所有浏览器都支持该功能,IE浏览器不能低于IE10。

整个CORS通信过程中,都是浏览器自动完成,对于开发者来说,CORS和同源的AJAX通信没有差别,浏览器一旦发现AJAX跨域,就会自动添加一些附加的头部信息,根据请求的不同还会多出一次附加的请求。

实现CORS通信的关键是服务器,只要服务器实现了CORS接口,就实现了跨域

服务器实现CORS通信的关键是设置以下请求头

  1. Access-Control-Allow-Origin,
    该字段时必选的,用于设置允许响应资源的origin
Access-Control-Allow-Origin: *  //允许所有域
Access-Control-Allow-Origin: <origin>   允许指定的origin
  1. Access-Control-Allow-Credentials 该字段是可选的,用于设置是否允许发送Cookie,该字段的值只能设为布尔值 ==true== ,删除该字段即可不发送。如果需要发送Cookie,这里还需要前端的AJAX请求设置 ==withCredentials== 属性,此外,Access-Control-Allow-Origin不能设为星号,必须明确指定与请求网页一致的域名,并且,Cookie依旧遵循同源策略,只有用服务器域名设置的Cookie才会上传,其他域名的Cookie并不会上传,且(跨源)原网页代码中的document.cookie也无法读取服务器域名下的Cookie。
var xhr = new XMLHttpRequest();
xhr.withCredentials = true;
  1. Access-Control-Expose-Header
    该字段可选,XMLHttpRequest对象的getResponseHeader()方法只能拿到6个基本字段:Cache-Control、Content-Language、Content-Type、Expires、Last-Modified、Pragma。如果想拿到其他字段,就必须在Access-Control-Expose-Headers里面指定。

CORS与JSONP比较

CORS与JSONP的使用目的相同,但是比JSONP更强大。

JSONP只支持GET请求,CORS支持所有类型的HTTP请求。JSONP的优势在于支持老式浏览器,以及可以向不支持CORS的网站请求数据。

03-02 02:08