跨域9大解决方案(超详细) 总结

前面的话

我们经常听到跨域这词,这是由于浏览器同源策略限制的一类请求场景。这样做的目的使得 浏览器不容易受到攻击。

什么是同源策略?

同源策略(Same origin policy)是一种约定,所谓同源是指“协议、域名、端口”三者都相同,如果没有同源策略,浏览器很容易受到XSS、CSRF等攻击。

同源策略限制了什么?

1)Cookie、LocalStorage和IndexDB无法获取
2) DOM 和JS对象无法获取
3)AJAX请求不能发送

不满足协议、域名、端口相同的都不能通信:
在这里插入图片描述

跨域的解决方法

  • CORS(跨域资源共享)
  • 通过jsonp跨域
  • document.domain + iframe跨域
  • location.hash + iframe跨域
  • window.name + iframe跨域
  • postMessage跨域
  • WebSocket协议跨域
  • nginx代理跨域
  • nodejs中间件代理跨域

CORS (Cross-origin resource sharing跨域资源共享)

CORS的背后基本思想:使用自定义的HTTP头部让浏览器与服务器进行沟通,从而决定请求响应是应该成功还是应该失败。

  • 使用node创建两个http服务器,监听端口8081和8084
  • cors.html运行在8081下

cors1.js服务,监听8081

var express = require('express');
var app = express();
// public下有cors.html页面
app.use(express.static('public')).listen(8081);

cors.html请求代码:

<script>
    let xhr = new XMLHttpRequest();
    document.cookie = 'name=xiaoqi';// cookie不能跨域
    xhr.withCredentials = true;// 前端设置是否带cookie
    xhr.open('PUT', 'http://localhost:8084/getData', true);
    xhr.setRequestHeader('name', 'xiaoqi');// 设置请求头
    xhr.onreadystatechange = function() {
        if(xhr.readyState===4 && xhr.status===200){
            console.log(xhr.response);
            // 返回响应头的name字段的值
            console.log(xhr.getResponseHeader('name'));
        }
    }
    xhr.send();
</script>

cors2.js服务,监听8084:

 var express = require('express');
var app = express();
// 设置白名单
let whiteList = ['http://localhost:8081'];
app.use(function(req, res, next) {
    let origin = req.headers.origin;
    if(whiteList.includes(origin)){
        // 设置哪个源可以访问我
        res.setHeader('Access-Control-Allow-Origin', origin);
        // 允许携带哪个头来访问我
        res.setHeader('Access-Control-Allow-Headers', 'name');
        // 允许哪个方法访问我
        res.setHeader('Access-Control-Allow-Methods', 'PUT');
        // 允许携带cookie
        res.setHeader('Access-Control-Allow-Credentials', 'true');
        // 预检的存在时间
        res.setHeader('Access-Control-Max-Age',6);
        // 允许返回的头
        res.setHeader('Access-Control-Expose-Headers', 'name');
        if(req.method === 'OPTIONS'){
            res.end()// OPTIONS请求不做任何处理
        }
    }
    next();
})
app.put('/getData', function(req, res){
    console.log(req.headers);
    res.setHeader('name', 'jw');
    res.end('hhh');
});
app.use(express.static('public1')).listen(8084);

显然8081与8084不在同一个域,在后端(cors2.js)代码中自动设置HTTP的响应头,来规定哪些域能访问。

上面代码的结果:
在这里插入图片描述

通过jsonp跨域

jsonp跨域原理是通过<script>标签来实现的。在HTML页面中通过相应的<script>标签从不同域名下加载资源,这种方式是被浏览器运行的。

注意: jsonp只能实现GET方法一种请求

我们也可以动态创建script标签,再请求一个带参网址实现跨域通信。

前端页面请求:

  • 原生实现
     <script>
        var script = document.createElement('script');
        script.type = 'text/javascript';
        script.src = 'http://localhost:8081?user=admin&callback=onBack';
        document.head.appendChild(script);
        // 回调函数
        function onBack(res) {
            // 把一个对象转为json的字符串类型
            alert(JSON.stringify(res));
        }
        // 服务器下的返回
        // onBack({"status": true,"user": "admin"});
    </script>
    
  • jquery ajax:
    $.ajax({
        url: 'http://localhost:8081',
        type: 'get',
        dataType: 'jsonp',  // 请求方式为jsonp
        jsonpCallback: "handleCallback",    // 自定义回调函数名
        data: {}
    });
    
  • vue.js
    this.$http.jsonp('http://localhost:8081', {
        params: {},
        jsonp: 'handleCallback'
    }).then((res) => {
        console.log(res); 
    })
    

后端node.js代码实现:

var qs = require('qs');
var querystring = require('querystring');
var http = require('http');
// 创建服务器
var server = http.createServer();

// 监听请求
server.on('request', function(req, res) {
    // qs.parse()将URL解析成对象的形式: params为{ user: 'admin', callback: 'onBack' }
    var params = qs.parse(req.url.split('?')[1]);
    var fn = params.callback;// 'onBack'
    
    // jsonp返回设置
    res.writeHead(200, {
        'Content-Type': 'text/javascript'
    })
    // res.write用来向请求的客户端发送响应的内容
    res.write(fn + '(' + JSON.stringify({"status": true, "user": "admin"})+ ')');
    res.end();
})

// 监听端口8081
server.listen('8081');
console.log('Server is running at port 8081...');

请求结果:
在这里插入图片描述

document.domain + iframe跨域

document.domain是用来得到当前网页的域名。

比如,在百度(https://www.baidu.com)页面控制台中输入:

alert(document.domain);  // `www.baidu.com`

我们也可以给document.domain属性赋值,不过是有限的, 只能赋值为当前域名或者一级域名

alert(document.domain = "baidu.com"); // `baidu`
alert(document.domain = "www.baidu.com"); // `www.baidu.com`

上面的赋值都是成功的:
在这里插入图片描述
下面的赋值则无效:

alert(document.domain = "qq.com"); // `baidu`
alert(document.domain = "www.qq.com"); // `www.baidu.com`

qq.com与baidu.com的一级域名不相同,所以出错。
在这里插入图片描述

如何使用document.domain + iframe实现跨域?
  • 实现原理:两个页面都通过js强制设置document.domain为基础主域,就实现了同域。
  • 前提条件: 这两个域名必须属于同一个一级域名,并且所用协议,端口相同,否则无法使用其方法来实现跨域。

注意:该方案仅限主域相同、子域不同的跨域场景

实例:

	news.baidu.com里的一个网页(news.html)引入了map.baidu.com里的一个
网页(map.html),这两个域是不相同的,但它们的主域都相同,都是baidu.com,
可以使用该方法实现跨域,将两个页面的domain都改为“baidu.com”.在news.html
与map.html中都加入:document.domain = "baidu.com"

代码:

new.baidu.com下的news.html:

 <iframe id="iframe1" src="http://map.baidu.com/map/html"></iframe>
 <script>
    document.domain = 'baidu.com';
    var iframe1  = document.getElementById("iframe1");
    // 返回iframe中的文档。兼容写法
    var doc = iframe1.contentDocument || iframe1.contentWindow.document;
    var p1 =  doc.getElementById('p1');
    alert(p1.innerHTML);
</script>

动态设置iframe的src更好,这样可以避免阻塞页面加载。

map.baidu.com下的map.html:

 <p id="p1"> 我是map.baidu.com中的p</p>
<script>
    document.domain = 'baidu.com';
</script>
iframe阻塞页面加载

这里iframe,就不得不说一下使用它的性能问题。

  • 及时触发 window 的 onload 事件是非常重要的。onload 事件触发使浏览器的 “忙” 指示器停止,告诉用户当前网页已经加载完毕。当 onload 事件加载延迟后,它给用户的感觉就是这个网页非常慢。

  • window 的 onload 事件需要在所有 iframe 加载完毕后(包含里面的元素)才会触发。在 Safari 和 Chrome 里,通过 JavaScript 动态设置 iframe 的 SRC 可以避免这种阻塞情况。

location.hash + iframe跨域

原理: 利用location.hash来进行传值。在url:http://a.com#helloword中的#helloword就是location.hash,改变hash并不会导致页面刷新,所以可以利用hash值来进程数据传递。

例如:在域名a.com下的a1.html文件要与域名b.com下的b.html文件进行数据传递。
步骤:

  • a1.html下创建一个隐藏的iframe(不影响页面格局),其src指向域名b.com下的b.html页面(http://b.com#hello) (#hello为hash值,用来传递数据)
  • b.html响应请求后,可以通过修改a1.html的hash值来传递数据。(只不过在不同域名下,IE、Chrome是不允许直接在b.html中直接通过parent.lacation.hash来改变a1.html的hash值;fireFox可以)。所以为了兼容,在IE、Chrome环境下,要借助a.com域名下的一个代理iframe(a2.html)。
  • 如果是IE、Chrome浏览器,就在b.html文件下创建一个隐藏的iframe,其src指向a2.html。通过parent.parent.location.hash来改变a1.html的hash值。

代码如下:

a.com下的a.html:

<script>
    // a1.html
    // 创建一个隐藏的iframe
    var iframe = document.createElement('iframe');
    iframe.src = "http://b.com/b.html#hello";
    iframe.style.display = 'none';
    document.body.appendChild(iframe);

    // 监听hash值
    function checkHash() {
        var data = location.hash? location.hash.substring(1): '';
        console.log(data);
    }
    // 每隔1s检测以下hash值是否变化
    setTimeout(checkHash,1000);
</script>

b.com下的b.html:

<script>
    // b.html
    // 模拟简单的location.hash值的改变
    switch(location.hash){
        case "#hello":
            // 如果hash值是hello,执行回调函数
            callback();
            break;
        case '#hhh':
          // ...
    }

    function callback(){
        try {
            // 如果是FireFox可以直接修改a1.html的hash值
            parent.location.hash = '#hello';
        
        } catch (error) {
            // 如果是在IE、Chrome下,就要创建a.com下的一个代理
            var iframe = document.createElement('iframe');
            iframe.style.display = 'none';
            // 将其hash值设置为b.html的hash值
            iframe.src = 'http://a.com/a2.html#hello';
            document.body.appendChild(iframe);
        }
    } 
</script>

域名a.com下的a2.html:

<script>
	// a2.html
    // a2.html与a1.html同域,可以通过a2.html来改变a1.html的hash值。
    parent.parent.location.hash = self.location.hash.substring(1);
</script> 

这种方法有很多缺点:数据都暴露在URL、数据容量有限等。

window.name + iframe跨域

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

利用这个特性,可以与iframe结合实现跨域。

实例:

  • 使用node创建两个http服务器,监听端口8081和8088
  • a.html运行在8081下
  • b.html运行在8088下

代码如下:

监听端口8081:

// 导入 express
const express = require('express');
const app = express();

// 将public目录导入,public下有a.html文件
app.use(express.static('public')).listen(8081);

localhost8081下的a.html代码:

<script>
    var iframe = document.createElement('iframe');
    iframe.src = "http://localhost:8088/b.html";
    iframe.style.display = 'none';
    var flag = 0;
    // onload会触发两次,第一次触发保存b.html下的数据,第二次重定向之后触发,读取同域的window.name
    iframe.onload = function() {
        if(flag == 1){
            // 返回b.html下的数据数据
            console.log("跨域资源", iframe.contentWindow.name);
        }else if(flag == 0) {
            // 得到b.html下的数据之后,立即将src设置为同域的proxy.html
            flag = 1;
            // iframe.src重定向
            iframe.contentWindow.location = "http://localhost:8081/proxy.html";
        }
    }
    document.body.appendChild(iframe);
</script>

上面的代码中将iframe.src重定向,指向了http://localhost:8081/proxy.html
,这个与a.html同域,并且就是一个空文件。为什么这么做?原因是:尽管window.name的值在不同域也存在,但是iframe有一个特性规定:如果a.html页面和该页面里的iframe框架的src不同源的话,就无法操作框架里的任何东西。 既然要同源,那就当保存了b.html下的name之后,将src指向一个同域的空页面,这样就可以顺利的读出b.html下的window.name值。

注意: 改变iframe的src之后会重新触发onload。

监听端口8088:

// 导入 express
const express = require('express');
const app = express();

// 将public目录导入,b.html文件里面
app.use(express.static('public1')).listen(8088);

localhost8088下的b.html代码:

 <script>
    window.name = "This is domain data";
</script>

结果:
在这里插入图片描述

postMessage跨域

postMessage是HTML5 XMLHttpRequest Level2中的API,且是为数不多可以跨域操作的window属性之一。
postMessage(data,origin)方法的使用:

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

实例:

还是像上面一样:

  • 使用node创建两个http服务器,监听端口8081和8088
  • aa.html运行在8081下
  • bb.html运行在8088下

localhost8081下的aa.html代码:

<script>
    var iframe = document.createElement('iframe');
    iframe.style.display = 'none';
    iframe.src = "http://localhost:8088/bb.html";
    document.body.appendChild(iframe);
    iframe.onload = function() {
        var data = {
            name: 'xiaoqi'
        }
        // 向bb.html传输跨域数据
        iframe.contentWindow.postMessage(JSON.stringify(data),"http://localhost:8088");
    }
    
    // 接受bb.html返回数据
    window.onmessage = function(event) {
        console.log('data from bb.html:' + event.data);
    }
    
</script>

localhost8088下的bb.html代码:

<script>
    // 接受aa.html的数据
    window.onmessage = function(event) {
        console.log('data from aa.html:' + event.data);
        // 将aa.html传来的数据转对象
        var data = JSON.parse(event.data);
        if(data) {
            data.number = 16;
        }
      
        // 将处理后的数据再发回aa.html
        window.parent.postMessage(JSON.stringify(data), 'http://localhost:8081');
    } 
</script>

结果:
在这里插入图片描述

WebSocket协议跨域

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

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

前端代码:

 <div>use input: <input type="text"> </div>
 <script src="https://cdn.bootcss.com/socket.io/2.2.0/socket.io.js"></script>
<script>
    // 通过io()生成客户端所需要的socket对象
    var socket = io('http://localhost:8081');

    //socket.on()用于接收服务器端发来的消息  连接成功处理
    socket.on('connect', function() {
        // 监听服务端消息
        socket.on('message', function(msg) {
            console.log('data from server:' + msg);
        });

        // 监听服务端关闭
        socket.on('disconnect', function() {
            console.log('Server socket has closed');
        });
    });
    // 
    document.getElementsByTagName('input')[0].onblur = function(){
        // 向服务端发消息
        socket.send(this.value);
    }
</script>

后端代码:

 var http = require('http');
var socket = require('socket.io');
// 启动http服务
var server = http.createServer(function (req, res) {
    res.writeHead(200,{
        'Content-type': 'text/html'
    });
    res.end();
})

server.listen(8081);
console.log('server is running at port 8081');

// 监听socket连接
socket.listen(server).on('connection', function (client){
    // 监听客户端传来的信息
    client.on('message', function (msg) {
        console.log('data from client:'+ msg);
        // 向客户端发消息
        client.send(msg);  
    })

    // 监听客户端断开连接 
    client.on('disconnect', function() {
        console.log('Client socket  has closed');
    });
});

在这里插入图片描述

nginx代理跨域与nodejs中间件代理跨域见参考文章3

参考:

发布了229 篇原创文章 · 获赞 169 · 访问量 4万+

猜你喜欢

转载自blog.csdn.net/qq_41257129/article/details/104047470