2020前端面试(四)-跨域篇

点这里,欢迎关注

一.同源策略

1.同源策略:

  • 同源策略是一种约定,它是浏览器最核心也最基本的安全功能。

  • 请求资源时必须保证同源,即"协议+域名+端口"三者必须相同,否则浏览器会因为安全性问题拦截请求到的数据。

  • 如果缺少了同源策略,浏览器很容易受到XSS、CSRF等攻击。

2.同源策略限制内容有:

  • Cookie、LocalStorage、IndexedDB 等存储性内容
  • Ajax请求

3.有三个标签是允许跨域加载资源:

  • <img src=XXX>
  • <link href=XXX>
  • <script src=XXX>

4.⭐️ 请求跨域了,那么请求到底发出去没有?

即使跨域了,请求也会发出去,服务端会收到请求并返回结果,只是最后结果被浏览器拦截了

二.跨域的解决方案:

  • 实现跨域的核心:绕过同源政策的限制

1.针对Ajax跨域请求数据的解决方案:

(1)jsonp方案:

原理:

  • 利用 <script> 标签可以跨域访问的特性,可以从服务器返回一段执行某函数的代码,并向里面传递结果数据。
  • jsonp请求需要对方的服务器做支持才可以。

jsonp和Ajax对比:

  • jsonp和Ajax都是发送请求向服务器请求数据的方法。
  • 但Ajax受同源策略的限制,jsonp属于跨域请求

jsonp的优缺点:

  • 优点是简单,兼容性好;可用于解决主流浏览器的跨域请求问题

  • 缺点:

    • 只支持get方法,因为script标签只能发送get请求

    • 不安全,可能遭受XSS攻击

    • 需要后端配合返回指定格式的数据

jsonp的实现流程:

  • 定义一个接受数据的回调函数。
  • 再封装一个函数,需要传递的参数包括回调函数的名称以及需要访问的url。
  • 在函数内部将url和回调函数的名字拼接起来,然后动态创建一个script标签,将src属性设置为拼接的字符串
  • 向dom树中添加这个script标签
  • 服务器端需要做的事:在拦截到用户请求时,获取请求参数中的回调函数名字,将欲返回的数据和该函数的名字拼接成函数调用的形式,然后返回给客户端。

jsonp方案的基本实现:

客户端:

<body>
    <button id="button">点击发送非同源的请求</button>
    <!-- 1.在全局作用域下定义函数fn,当加载完后面的script标签后,相当于调用了这个fn函数,所以fn函数里的data就变成了我们想要获取的数据。 -->
    <script>
        function fn(data) {
            console.log(data);
        }
    </script>
    <script>
        button.onclick = function () {
            let script = document.createElement("script");
            script.src = "http://localhost:3001/jsonpBasic";
            document.body.appendChild(script);
            // 2.存在一个问题:每次点击发送都会为dom添加一个script标签。
            // 解决方案:在每次加载完script标签后,将其从html中删除
            script.onload = function () {
                document.body.removeChild(script);
            };
        };
    </script>
</body>

服务器端:

//1.由于script标签的原理是一加载就会执行里面的代码,故服务器端响应的数据只能是一个执行函数的表达式,这样客户端才能拿到想要的数据。
//2.注意:通过script标签发送的请求都是get请求。
app.get("/jsonpBasic", (req, res) => {
    res.send('fn({name:"zhangsan",age:20})');
});

jsonp函数的封装:

一些优化点:
1.每次点击发送都会为dom添加一个script标签
2.客户端和服务端需要商讨函数的名称的问题

3.封装jsonp函数

4.如何省去函数的定义,并且调用Jsonp函数的时候省去url后面的字符串

5.请求参数的问题

6.服务器代码的优化:利用res.jsonp({})可以直接代替所有步骤。。。

客户端:

<button id="button">点击发送非同源的请求</button>
<!-- 1.在全局作用域下定义函数fn,当加载完后面的script标签后,相当于调用了这个fn函数,所以fn函数里的data就变成了我们想要获取的数据。 -->
<script>
    //   function fn3(data) {
    //     console.log(data);
    //   }
    button.onclick = function () {
        jsonp({
            url: "http://localhost:3001/jsonp",
            data: {
                name: "zhangsan",
                age: 20,
                gender: "male",
            },
            success: function (data) {
                console.log(data);
            },
        });
    };
    //   4.封装jsonp函数,跟封装ajax类似
    function jsonp(options) {
        let script = document.createElement("script");

        let params = "";
        for (key in options.data) {
            params += `&${key}=${options.data[key]}`;
        }
        // 5.将传递过来的函数挂载在window对象上,使其成为全局函数,便于加载script标签的时候直接执行这个函数。
        // 这里存在一个问题:后发送的请求中挂载的fn2函数会覆盖前面的fn2函数。解决方法是将这个函数的名字设置一个随机数。
        //0.1524 注意:函数名不能为纯数字
        let functionName =
            "myJsonp" + Math.random().toString().replace(".", "");
        // window.fn2 = options.success;
        window[functionName] = options.success;
        // 3.在请求路径中添加所使用的函数的名称,解决需要前后端需要商量函数名的问题
        // script.src = options.url + "?callback=fn3";
        script.src = `${options.url}?callback=${functionName}${params}`;
        document.body.appendChild(script);
        // 2.存在一个问题:每次点击发送都会为dom添加一个script标签。
        // 解决方案:在每次加载完script标签后,将其从html中删除
        script.onload = function () {
            document.body.removeChild(script);
        };
    }
</script>

服务器端:

app.get("/jsonp", (req, res) => {
    //1.由于script标签的原理是一加载就会执行里面的代码,故服务器端响应的数据只能是一个函数,这样客户端才能拿到想要的数据。
    //2.注意:通过script标签发送的请求都是get请求。
    // let functionName = req.query.callback;
    // // let data = 'fn({username:"zhangsan",age:20})';
    // let data = `${functionName}({username:"zhangsan",age:20})`;
    // res.send(data);

    // 3.使用res.jsonp({})可以代替所有的步骤
    res.jsonp({ username: "zhangsan", age: 20 });
});
(2)CORS方案:

CORS:cross-origin resource sharing跨域资源共享

原理:

  • 浏览器允许 由服务器端决定哪些客户端能跨域访问其资源。
  • 服务器端会在响应报文中设置Access-Control-Access-Origin字段,表示该资源能被哪些客户端跨域访问。
  • 后端是实现CORS通信的关键
origin: http://localhost:3000

Access-Control-Allow-Origin: "*"  表示该资源允许所有的客户端访问

服务器端需要进行的设置:

app.get("/cors", (req, res) => {
    // 1.设置该资源能被哪些客户端访问
    res.header("Access-Control-Allow-Origin", "*");
    // 2.设置访问该资源时所允许的客户端发送请求的方式
    res.header("Access-Control-Allow-Methods", "get,post");
    res.send({ name: "zhangsan" });
});

优化:利用expree框架的中间件拦截所有的请求,使得客户端发送的所有请求都能实现跨域访问。

app.use((req, res, next) => {
    // 1.设置该资源能被哪些客户端访问
    res.header("Access-Control-Allow-Origin", "*");
    // 2.设置访问该资源时所允许的客户端发送请求的方式
    res.header("Access-Control-Allow-Methods", "get,post");
    next();
});
(3)nginx反向代理
  • 实现原理类似于Node中间件代理,需要你搭建一个中转nginx服务器,用于转发请求。

  • 使用nginx反向代理实现跨域,是最简单的跨域方式。只需要修改nginx的配置即可解决跨域问题

// proxy服务器
server {
    listen       80;
    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;
    }
}
(4)Node中间件代理(两次跨域)

原理:

  • 服务器端不存在同源政策的限制
  • 可以由客户端访问本地代理服务器端,由本地代理服务器端去访问2号服务器端的数据
  • 本地代理服务器获取数据后再将数据返回给客户端。

如何在node服务器中向另一个node服务器发送请求:

  • 需要用到第三方模块,比如’superagent’,在同源的服务器上利用superagent向另一个服务器发送请求。
(5)websocket方案:(直接不使用Ajax,替换为websoket协议 )

原理:

  • Websocket是HTML5的一个持久化的协议,它实现了浏览器与服务器的全双工通信,同时也是跨域的一种解决方案。可以直接跨域访问数据。

具体实现:

  • 客户端:

    // socket.html
    <script>
        let socket = new WebSocket('ws://localhost:3000');
        socket.onopen = function () {
          socket.send('服务器你好啊');//向服务器发送数据
        }
        socket.onmessage = function (e) {
          console.log(e.data);//接收服务器返回的数据
        }
    </script>
    
  • 服务器端:

    // server.js
    // 原生WebSocket API使用起来不太方便,我们可以使用Socket.io,它很好地封装了webSocket接口
    let express = require('express');
    let app = express();
    let WebSocket = require('ws');//记得安装ws
    let wss = new WebSocket.Server({port:3000});
    wss.on('connection',function(ws) {
      ws.on('message', function (data) {
        console.log(data);
        ws.send('嘿嘿,客户端你好')
      });
    })
    

2.跨域获取另一个页面(比如iframe)的数据:

(6)postMessage方案

原理:

  • postMessage是HTML5提供的新的API,可以安全地实现跨源通信。
  • 具体实现是通过window.postMessage发送消息,通过onMessage来监听消息

实际应用场景包括:

  • 页面与嵌套的iframe之间的通信
  • 多个窗口之间的通信

具体实现:(注意:iframe相当于只提供了一个窗口,通信的实现靠的是postMessage这个API)

  • tab1: 引入iframe的页面
<body>
    <input type="text" id="input" />
    <button onclick="sendMessage()">点击向iframe发送数据</button><br />
    <iframe
            src="http://127.0.0.1:5500/3.test2.html"
            frameborder="0"
            allow=""
            id="iframe"
            ></iframe>
</body>
<script>
    function sendMessage() {

        // 1.获取iframe窗口的引用

        // 方法一:直接获取iframe的contentWindow属性
        // contentWindow 属性返回当前HTMLIFrameElement的Window对象. 你可以使用这个Window 对象去访问这个iframe的文档和它内部的DOM
        // var win = iframe.contentWindow;

        // 方法二:通过window.frames获取
        // 返回的是frame对象的集合,一个类数组对象,列出了当前窗口的所有直接子窗口
        var win = frames[0];

        // 2.向指定的窗口发送数据
        //otherWindow.postMessage(message, targetOrigin);
        //2.1 otherwindow表示指定的窗口,可以有三种情况:
        //可以是iframe的contentWindow属性
        //也可以是window.open返回的窗口对象
        //还可以是window.frames中的子窗口        
        //2.2 第一个参数message表示要发送的消息
        //2.3 第二个参数表示目标窗口的地址。注意:如果是/,则表示只发送消息给同源的页面;如果是*就表示将数据发送给全部页面。
        win.postMessage(input.value, "http://127.0.0.1:5500");
    }
</script>
  • tab2: iframe原页面
<body>
    <div
         id="iframe"
         style="width: 500px; height: 500px; background-color: skyblue"
         >
        这是内嵌的iframe
    </div>
</body>
<script>
    //   监听通过postMessage向自己发来的数据
    window.addEventListener("message", function (e) {
        console.log(e);
        // e.target表示发送方的原地址
        // e.data表示发送过来的数据
        // e.source表示对发送消息的窗口对象(即window对象)的引用,可以利用继续使用这个属性向原地址返回数据			 
        // e.source.postMessage()
        iframe.innerHTML = e.data;
    });
</script>
(7)window.name + iframe

前置条件:

  • iframe中可以加载跨域的资源,但是获取iframe中的数据是属于跨域的。
  • ⭐️ 核心:在同一个窗口中,即使页面被换为了其他页面,该窗口的window.name属性依旧不变。

原理:

  • 首先让iframe加载跨域的页面,此时跨域的页面需要做的就是将数据存放在window.name属性中
  • 然后让iframe中的src指向一个同源的空页面,此时由于是同源的情况,所以可以通过iframe.contentWindow访问到里面的name属性,这样就拿到了跨域页面传递过来的数据

具体代码:

  • 原页面:http://127.0.0.1:5500/1.test.html
<body>
    <iframe frameborder="0" id="iframe1"></iframe>
    <script>
        console.log(iframe1.contentWindow.name); //输出为空----由于跨域,不能直接获取跨域页面的属性

        let tag = 0;
        iframe1.src = "http://localhost:3000/index2.html"; //首先加载跨域的页面,使其将数据存储在window.name中

        iframe1.onload = function () {
            if (tag == 0) {
                iframe1.src = "http://127.0.0.1:5500/proxy.html"; //将src的路径切换为同源的路径,此时由于是同源的缘故,可以获取其中窗口的window.name属性
                tag = 1;
            } else {
                console.log(JSON.parse(iframe1.contentWindow.name));//{name: "liu", age: 21, gender: 1}
            }
        };
    </script>
</body>
  • 中间代理页面:http://127.0.0.1:5500/proxy.html
<body></body> 是个空页面
  • 跨域访问的页面:http://localhost:3000/index2.html
<body>
    这里是跨域请求的iframe页面
    <script>
        let obj = {
            name: "liu",
            age: 21,
            gender: 1,
        };
        window.name = JSON.stringify(obj);
    </script>
</body>
(8)location.hash + iframe

前置条件:

  • 当一个窗口的hash(url后面的#部分)改变时,会触发hashchange事件

原理:

  • 首先通过iframe加载跨域的页面
  • 跨域的页面创建一个iframe,src设置为与原来页面同源的一个页面。关键是需要url后添加一个hash,值为传递的数据
  • 同源的代理页在被加载时,需要将其身上的location.hash设置在父级window的父级window的location.hash上。
  • 当原页面监听到hashchange事件时,从中取出location.hash就可以得到最终的数据

具体实现:

  • 原页面:http://127.0.0.1:5500/1.test.html

    <iframe frameborder="0" id="iframe1"></iframe>
    <script>
        iframe1.src = "http://localhost:3000/index2.html";
        // 原页面需要监听hash值得改变,即监听此时代理页是否将location.hash设置好了
        window.addEventListener("hashchange", function (e) {
            console.log(location.hash.substring(1));
        });
    </script>
    
  • 跨域请求的页面:http://localhost:3000/index2.html

    <body>
        这里是跨域请求的iframe页面
        <script>
            let obj = {
                name: "liu",
                age: 21,
                gender: 1,
            };
            var iframe = document.createElement("iframe");
            iframe.src =
                "http://127.0.0.1:5500/proxy.html" + "#" + JSON.stringify(obj); //在服务器端创建一个iframe,src指回原来的域名,hash值设置为响应的数据
            document.body.appendChild(iframe);
        </script>
    </body>
    
  • 同源的代理页面:http://127.0.0.1:5500/proxy.html

    <body>
        <script>
            // 代理页即为服务器端内嵌的iframe
            // 其中可以访问到传递过来的hash值,可以将其设置在上级window的上级window的location.hash属性中
            window.parent.parent.location.hash = location.hash.substring(1);
        </script>
    </body>
    
(9)document.domain + iframe

前置条件:

  • 若两个页面的域名必须属于同一个基础域名,而且所用的协议,端口都要一致,则可以实现跨域访问。
  • 具体实现就是通过js强制性的将两个页面的document.domain属性设置为相同的主域名。

具体实现:

  • 原页面:http://a.zf1.cn:3000/a.html

    <body>
        <iframe src="" id="iframe"></iframe>
        <script>
            document.domain="zf1.cn";
        	iframe.src="http://b.zf1.cn:3000/b.html";
            iframe.onload=function(){
                console.log(iframe.contentWindow.age)//21
            }
        </script>
    </body>
    
  • 跨域的页面: http://b.zf1.cn:3000/a.html

    <body>
        <script>
            document.domain="zf1.cn";
        	var age=21;
        </script>
    </body>
    

3.总结

  • CORS支持所有类型的HTTP请求,是跨域HTTP请求的根本解决方案
  • jsonp只支持GET请求,JSONP的优势在于兼容性很好。
  • 日常工作中,用得比较多的跨域方案是CORS和nginx反向代理
  • 不管是Node中间件代理还是nginx反向代理,利用的都是服务器端不存在同源政策的限制

参考链接

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值