深入前端跨域的9种解决方法和原理

1,448 阅读9分钟

跨域是什么?

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

为什么有跨域:

出于安全原因,浏览器有同源限制,限制从脚本内发起跨源的HTTP请求,

例如: XMLHttpRequest和Fetch API遵循同源策略。 ​这意味着使用这些API的Web应用程序只能从加载应用程序的同一个域请求HTTP资源, 除非响应报文包含了正确CORS响应头。[解决跨域]

什么是XMLHttpRequest:

XMLHttpRequest是一个浏览器接口,使得Javascript可以进行HTTP(S)通信。 最早,微软在IE 5引进了这个接口。因为它太有用,其他浏览器也模仿部署了,ajax操作因此得以诞生。 但是,这个接口一直没有标准化,每家浏览器的实现或多或少有点不同。 HTML 5的概念形成后,W3C开始考虑标准化这个接口。 2008年2月,就提出了XMLHttpRequest Level 2 草案。 这个XMLHttpRequest的新版本,提出了很多有用的新功能,将大大推动互联网革新。 参考: XMLHttpRequest Level 2 使用指南

什么是AJAX :

AJAX = Asynchronous JavaScript and XML(异步的 JavaScript 和 XML)。 AJAX 不是新的编程语言,而是一种使用现有标准的新方法。 AJAX 是与服务器交换数据并更新部分网页的艺术,在不重新加载整个页面的情况下。

什么是同源策略:

具体可以看浏览器同源政策及其规避方法 的这边文章 写的很好,想简单了解下的可以看下面的

1.1 定义:

  • 协议
  • 域名
  • 端口

一致才叫同源

1.2 目的:

是为了保证用户信息的安全,防止恶意的网站窃取数据。

1.3 限制范围,目前三种行为受限制

1. Cookie、LocalStorage 和 IndexDB 无法读取。

【单点登录的同源实现方式就是通过设置cookie的domain】

  • cookie: 只有同源网页才能共享
    • 一级域名相同,二级不相同,可以设置document.domain
    • 服务器指定所属cookie的一级域名Set-Cookie: key=value; domain=.example.com; path=/
      • 这样的话二级三级都可以读取这个cookie
  • LocalStorage:
    • window.postMessage ( HTML5 引入的新的API ) : 允许窗口通信
    • 子父窗口都可以通过监听message事件
  • indexDB: 同上
2. DOM 无法获得。
  • iframe: 无法与父窗口通信
    • 一级域名相同,二级不同,可以设置document.domain
    • 完全不同源
      • 片段标识符 #
        • url #后的部分,改变片段标识符,页面不会刷新
          • 父窗口可以把信息写入子窗口的片段标识符
          • 子窗口监听hashchange事件
      • window.name
        • 缺点:必须监听子窗口的window.name ,影响性能
        • 优点:容量很大,可以防止很长的字符串
      • 跨文档通信API window.postMessage
  • window.open: 同上
3. AJAX 请求不能发送。

解决跨域的几种主要方法

一、AJAX(核心)

1. JSONP跨域原理分析

思路:

利用scriptd 的src不受同源策略限制,动态生成script。和后端约定相同的函数名,服务端返回的数据包裹在函数名里。缺点是只支持get。

2. CORS(cross-origin resource sharing)跨域资源共享

大概思路:
  1. 如果是简单请求比如get,post,head 等浏览器会在请求头里自动加一个origin,服务端需要返回Access-Control-Allow-Origin 字段 , 代表允许所有跨域,也可以是指定域名。*
  2. 如果是非简单请求比如说put、delete、content-type: application/json 会发出预检请求options,在正式请求前先问问服务器当前网页是否在名单内,是的话发出正式请求,不是的话报错,有有效期字段 代表多久内不需要发options请求

总之:需要服务端的配合

3. websocket 跨域

JS如何将HTTP如何升级成websocket

WebSocket protocol是HTML5一种新的协议。它实现了浏览器与服务器全双工通信,同时允许跨域通讯,是server push技术的一种很好的实现。

服务端推送技术还有HTML5新增的WEB API :SSE单向事实服务端推送和 【HTTP2的服务端推送预资源机制】

原生WebSocket API使用起来不太方便,我们使用Socket.io,它很好地封装了webSocket接口,提供了更简单、灵活的接口,也对不支持webSocket的浏览器提供了向下兼容。


二、 服务器代理:浏览器请求同源服务器,再由后者请求外部服务

同源策略仅是针对浏览器的安全策略。 服务器端调用HTTP接口只是使用HTTP协议,不需要同源策略,也就不存在跨域问题。 实现思路: 通过Nginx配置一个代理服务器域名与domain1相同,端口不同)做跳板机,反向代理访问domain2接口,并且可以顺便修改cookie中domain信息,方便当前域cookie写入,实现跨域访问。

1. nginx 代理跨域

location / {
  add_header Access-Control-Allow-Origin *;
}

#proxy服务器
server {
    listen       81;
    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 http://www.domain1.com;  #当前端只跨域不带cookie时,可为*
        add_header Access-Control-Allow-Credentials true;
    }
}

2. nodejs 中间件代理跨域

node中间件实现跨域代理,原理大致与nginx相同。 都是通过启一个代理服务器,实现数据的转发。 也可以通过设置cookieDomainRewrite参数修改响应头中cookie中域名,实现当前域的cookie写入,方便接口登录认证。

  • 非VUE: 使用node + express + http-proxy-middleware搭建一个proxy服务器。
# 前端代码
var xhr = new XMLHttpRequest();

// 前端开关:浏览器是否读写cookie
xhr.withCredentials = true;

// 访问http-proxy-middleware代理服务器
xhr.open('get', 'http://www.domain1.com:3000/login?user=admin', true);
xhr.send();

# 中间件服务器代码:
var express = require('express');
var proxy = require('http-proxy-middleware');
var app = express();

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

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

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

app.listen(3000);
console.log('Proxy server is listen at port 3000...');
  • VUE:node + vue + webpack + webpack-dev-server
# webpack.config.js部分配置:
# 开发环境下,vue渲染服务和接口代理服务都是webpack-dev-server同一个,所以页面与代理接口之间不再跨域。

module.exports = {
    entry: {},
    module: {},
    ...
    devServer: {
        historyApiFallback: true,
        proxy: [{
            context: '/login',
            target: 'http://www.domain2.com:8080',  // 代理跨域目标接口
            changeOrigin: true,
            secure: false,  // 当代理某些https服务报错时用
            cookieDomainRewrite: 'www.domain1.com'  // 可以为false,表示不修改
        }],
        noInfo: true
    }
}

三、窗口

1. 设置document.domain :一级相同,二级不同的情况下

此方案仅限主域相同,子域不同的跨域应用场景。实现原理:两个页面都通过js强制设置document.domain为基础主域,就实现了同域。

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

2. hash路由 片段标识符(#)

实现原理: a欲与b跨域相互通信,通过中间页c来实现。 三个页面,不同域之间利用iframe的location.hash传值,相同域之间直接js访问来通信。

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

3. window.name:

window.name属性的独特之处:name值在不同的页面(甚至不同域名)加载后依旧存在,并且可以支持非常长的 name 值(2MB)

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);
});

4. window.postMessage

postMessage是HTML5 XMLHttpRequest Level 2中的API,且是为数不多可以跨域操作的window属性之一,它可用于解决以下方面的问题:

  • 页面和其打开的新窗口的数据传递
  • 多窗口之间消息传递
  • 页面与嵌套的iframe消息传递
  • 上面三个场景的跨域数据传递

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

  • data: html5规范支持任意基本类型或可复制的对象,但部分浏览器只支持字符串,所以传参时最好用JSON.stringify()序列化。
  • origin: 协议+主机+端口号,也可以设置为"*",表示可以传递给任意窗口,如果要指定和当前窗口同源的话设置为"/"。

a.html:(www.domain1.com/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');
    };

    # 同样监听message 接受domain2返回数据
    window.addEventListener('message', function(e) {
        alert('data from domain2 ---> ' + e.data);
    }, false);
</script>

b.html:(www.domain2.com/b.html)

# 监听message  接收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);

重点: 1向2传递 : iframe.contentWindow.postMessage 2向1返回:window.parent.postMessage 两边都监听message 接受收到的数据

总结:

  • ajax相关的有三个,最常用的是CORS,JSONP只支持get,都需要服务端配合
  • 窗口有关的有4个,postMessage 最全面,domain的设置需要主域一样,name性能不好
  • 服务器代理相关的原理都是一样的,比较适合前后端分离的项目,需要有一定的经验

没有最好, 只有适合 ​

参考文章: