计网基础-跨域

loading 2022年11月24日 68次浏览

1. 引入

1.1 同源策略

同源策略是一种约定,是浏览器最基本且核心地安全功能。所谓同源是指协议+域名+端口三者相同,即使两个不同的域名指向同一个ip地址,也非同源。

示例,现有一URL:
http://store.company.com/dir/page.html

//同源 因为只有路径不同
http://store.company.com/dir2/other.html

//同源 因为只有路径不同
http://store.company.com/dir/inner/another.html

//失败 因为协议不同(这里是https)
https://store.company.com/secure.html

//失败 因为端口不同(http默认端口是80)
http://store.company.com:81/dir/etc.html

//失败 因为域名不同
http://news.company.com/dir/other.html

同源策略限制了以下内容:

  • Cookie,LocalStorage,IndexedDB等存储不能读取
  • DOM节点无法获得
  • AJAX请求会被浏览器拦截

这三个标签允许跨域加载资源(省略了前面的< 不然md格式显示不出来)
img src=xxx>
link href=xxx>
script src=xxx>

1.2 常见跨域场景

不同源之间互相请求资源,就算做“跨域”,跨域的目的就是为了阻止用户读取到不同域下的内容。

如果是协议和端口造成的跨域问题,前端是无能为力的。

要注意的是,跨域并不是请求发不出去,而是请求发出去了,服务端接收到请求且返回结果了,但是结果被浏览器拦截了

2.跨域解决方案

2.1 JSONP

2.1.1 概念和特点

JSONP是JSON的一种使用模式,利用script标签没有跨域限制的漏洞,网页可以得到其它源动态产生的JSON数据,但是JSONP一定要对方的服务器做支持才可以。

JSONP和AJAX相同点在于,都是从客户端向服务端发送请求,再从服务端获取数据。但AJAX属于同源策略,JSONP属于跨域请求。

JSONP的优缺点如下:

优点是简单且兼容性好,可用于解决主流浏览器的跨域数据访问的问题。

缺点是只支持get方法,导致可能会遭受XSS攻击。

2.1.2 实现流程

(1) 前端定义一个回调函数
(2) 在script的url中通过参数将函数传给后端
(3) 后端获取到前端传过来的函数,带上要回传的参数执行该函数
(4) 前端在script标签返回资源时就会去执行回调函数并拿到数据

先展示一个最基础的例子:

虽然两个文件端口号不同,但是还是成功访问到了程序B中的message,弹出success,体现了script标签的src属性不受同源策略约束。

程序A

    <script src="http://domain/api?param1=a&param2=b&callback=jsonp"></script>
    <script>
        //回调函数
        function jsonp(data) {
            alert(data.message);
        }
    </script>

程序B

	//调用callback函数,并以json数据形式作为阐述传递,完成回调
	jsonp({message:"success"});

像上面这个例子,对应着上面的实现流程来看:

(1) 定义好了jsonp这个回调函数,执行到第二个script时由于通过url请求了这个地址,那么后端就能拿到url里的参数
(2) 后端拿到函数参数也就是callback=jsonp后,就知道了前端想通过这个回调函数接收数据,因此将要回传的数据作为回调函数的参数并执行
(3) 前端得到了后端返回的内容jsonp({message:"success"})发现这是一个函数,于是执行该函数并弹出message

2.1.3 封装jsonp

一般情况下,我们希望这个script标签能够动态的调用,而不是像上面因为固定在html里面,没等页面显示就执行了,很不灵活,同时还可能遇到请求的回调函数名相同的情况。

我们可以通过javascript动态的创建script标签,这样我们就可以灵活调用远程服务了。

因此我们可以自己封装一个jsonp,然后传入相应的参数来执行:

        function myJsonp(url, jsonpCallback, success) {
            let script = document.createElement('script')
            script.src = url
            script.async = true
            script.type = 'text/javascript'
            window[jsonpCallback] = function (data) {
                success && success(data)
            }
            document.body.appendChild(script)
        }

        myJsonp('http://xxx', 'callback', function (value) {
            console.log(value)
        })

举个实际中动态创建script的例子,比如谷歌的ajax搜索接口:

    <script>
        //添加<script>标签的方法
        function addScriptTag(src) {
            var script = document.createElement('script');
            script.setAttribute("type", "text/javascript");
            script.src = src;
            document.body.appendChild(script);
        }

        window.onload = function () {
            //搜索apple,将自定义的回调函数名result传入callback参数中
            addScriptTag("http://ajax.googleapis.com/ajax/services/search/web?v=1.0&q=apple&callback=result");
        }

        //自定义的回调函数result
        function result(data) {
            //我们就简单的获取apple搜索结果的第一条记录中url数据
            alert(data.responseData.results[0].unescapedUrl);
        }
    </script>

2.2 CORS

cors主要由后端支持。

浏览器会自动进行CORS通信,实现CORS通信的关键是后端。只要后端实现了CORS,就实现了跨域。

服务端设置Access-Control-Allow-Origin就可以开启 CORS。该属性表示哪些域名可以访问资源,如果设置通配符则表示所有网站都可以访问资源。

2.2.1 简单请求和复杂请求

虽然设置CORS和前端没什么关系,但是通过这种方式解决跨域问题的话,会在发送请求时出现两种情况,分别为简单请求和复杂请求

简单请求需要同时满足若干个条件:
(1)使用下列方法之一:

  • GET
  • HEAD
  • POST

(2)Content-Type的值为下列之一:

  • text/plain
  • multipart/form-data
  • application/x-www-form-urlencoded

(3)请求中的任意XMLHttpRequestUpload对象均没有注册任何事件监听器;XMLHttpRequestUpload对象可以使用XMLHttpRequest.upload属性访问。
等等

满足浏览器的简单请求会直接发送,并在请求头中携带Origin表示本次来自哪个源

<script src="https://cdn.bootcss.com/axios/0.19.2/axios.min.js"></script>
<script>
  axios.get("http://127.0.0.1:8080/api/corslist");
</script>

不符合上面条件的就是复杂请求。

会在发送请求之前发送一个预检请求,方法为OPTIONS,只包含头部信息,不包含请求体,单独一个包发给服务器。包含如下字段:

预检请求就是试探一下服务器那边是否允许自己发请求。
如果被拒绝了就不请求了,节省资源。

收到服务器响应后,分析它是否支持跨域,如果支持跨域则发送复杂请求,如果不支持则报错。

这里我们加入了一个非集合内的header头cc来达到实现复杂请求的目的。

<script src="https://cdn.bootcss.com/axios/0.19.2/axios.min.js"></script>
<script>
  axios.get("http://127.0.0.1:8080/api/corslist", { header: { cc: "xxx" } });
</script>

2.2.2 关于cors的cookie问题

想要传递cookie需要满足三个条件:

(1) 请求头设置withCredentials
(2) 响应头设置Access-Control-Allow-Credentials为true
(3) 响应头设置Access-Control-Allow-Origin为非*

2.3 websocket

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

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

2.4 postMessage

postMessage是HTML5 XMLHttpRequest Level 2中的API,且是为数不多可以跨域操作的window属性之一

postMessage()方法允许来自不同源的脚本采用异步方式进行有限的通信,可以实现跨文本档、多窗口、跨域消息传递。

otherWindow.postMessage(message, targetOrigin, [transfer]);

2.5 Node正向代理

代理的思路为,利用服务端请求不会跨域的特性,让接口和当前站点同域。

  • 接收客户端请求
  • 将请求转发给服务器
  • 拿到服务器返回的数据
  • 将数据转发给客户端

这里有很多种,比如自己项目里用到的proxy代理,具体的应用到时候写项目博客的时候讲解一下。

2.6 nginx反向代理

实现原理类似于Node中间件代理,需要你搭建一个中转nginx服务器,用于转发请求。也就是自己搭建的中转服务器先向需要包含数据的服务器获取数据,再将获取到的数据传回给自己的客户端。

做javaweb项目时常用到使用nginx反向代理实现跨域,是最简单的跨域方式。只需要修改nginx的配置即可解决跨域问题,支持所有浏览器,支持session,不需要修改任何代码,并且不会影响服务器性能。

而在react中使用跨域,可以通过以下方法实现:
https://create-react-app.dev/docs/proxying-api-requests-in-development/

(1) npm install http-proxy-middleware --save

(2) src目录下建立setupProxy.js

(3) 将模板代码复制到文件中,然后根据具体需求作修改(targer是目标地址,'/api'是目标地址后的前缀,如:http://localhost:5000/api/xxx/xxx)

const { createProxyMiddleware } = require('http-proxy-middleware');

module.exports = function(app) {
  app.use(
    '/api',
    createProxyMiddleware({
      target: 'http://localhost:5000',
      changeOrigin: true,
    })
  );
};

2.7 iframe

三种:

  • window.name + iframe
  • location.hash + iframe
  • document.domain + iframe