计算机网络http|面试中🍕🍕常问的几种处理跨域的方法

1,036 阅读8分钟

请求的几种方式

get:(select)从服务器请求数据,(给的少拿的多)

post:(create)向服务器发送数据,(给的多拿的少)

put:向服务器存放一些内容

delete:删除服务器的一些内容

head:只请求页面的头部

trace:回显服务器收到的请求,主要用于测试或诊断

options:允许客户端查看服务器的性能

connect:HTTP/1.1协议中预留给能够将连接改为管道方式的代理服务器

image.png

什么是同源策略

image.png

  1. 同源策略 必须是同协议、同域名、同端口

什么是跨域

当前页面中的某个接口请求的地址和当前页面的地址如果协议、域名、端口其中有一项不同,就说该接口跨域了

我们来看下面的页面是否与 http://store.company.com/dir/index.html 是同源的?
http://store.company.com/dir/index2.html 同源
http://store.company.com/dir2/index3.html 同源 虽然在不同文件夹下
https://store.company.com/secure.html 不同源 不同的协议(https)
http://store.company.com:81/dir/index.html 不同源 不同的端口(81)
http://news.company.com/dir/other.html 不同源 不同的域名(news)

跨域的解决方法

1、 通过jsonp跨域

2、 window.name+ iframe跨域

3、 location.hash + iframe跨域

4、 document.domain + iframe跨域

5、 postMessage跨域

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

7、 nginx代理跨域

8、 nodejs中间件代理跨域(node + webpack + webpack-dev-server)

9、 WebSocket协议跨域

详细介绍各个方法

(1)Jsonp 分为原生的jsonp和jQuery的jsonp

(2)CORS是一个W3C标准,全称是"跨域资源共享"(Cross-origin resource sharing),主要设置在后端,只要后端实现了跨域,就实现了跨域

(3)Node中间件代理(两次跨域)

(4) Nginx反向代理

在非同源时,以下情况受到限制,无法共享

(1)Cookie、LocalStorage、IndexedDB等存储性内容

(2) DOM节点

(3) AJAX请求不能发送

在非同源时,以下情况不受同源限制(允许跨域加载)

(1) <img src=xxx> (2) <link href=xxx> (3)<script src=xxx>

jsonp的解决方式

(1) 原生的方式

如果a页面(a.com/jsonp.html) 想要得到b页面(b.com/main.js)里的数据(很明显a,b在不同的域名下,需要跨域)

在jsonp.html页面创建一个回调函数,动态的添加script元素,向服务器发送请求,请求地址为 http://b.com/main.js?callbcak=xxx,在main.js里面调用这个回调函数,并且以json的数据格式作为参数传递,完成回调。

/**
 * jsonp.html
 */
let script = document.createElement('script');

script.src = 'http://b.com/main.js?callback=foo';

document.body.appendChild(script);

function foo(res) {

  console.log(res);

}
/**
 * main.js
 */
foo({name:'tom'})

(2) JQuery中实现跨域

JQuery会自动生成一个全局函数来代替callback=?中的问号,之后获取数据以后又会自动销毁,实际上就是起到一个临时函数的作用,$.getJSON会自动判断跨域,如果不存在跨域,会自动调用ajax,如果存在跨域,会利用异步加载的方式调用Jsonp中的回调函数

//jquery实现方式(版本一)

<script>
$.getJSON('http://example.com/data.php?callback=?', function (data) {
    console.log(data);
});
</script>
//jquery实现方式(版本二)

$.ajax({

    url:'http://www.xxx.com/login',

    type:'GET',

    dataType:'jsonp',//请求方式为jsonp

    jsonpCallback:'callback',

})

function callback(res) {

  console.log(res);

}

jsonp的缺点

(1) 只要用这种方式,任何页面都可以访问b页面的数据,存在安全性问题

(2)只能发送get请求,不能发送post请求

(3)可能会被注入恶意代码,篡改页面内容

使用CROS方式

浏览器将CORS跨域请求分为简单请求和非简单请求。 只要同时满足这两个条件,就属于简单请求

(1)方法: head,get,post

(2)Heder: Accept、Accept-Language、Content-Language、Content-Type(只限于三个值:application/x-www-form-urlencoded、multipart/form-data、text/plain )

简单请求

对于简单请求,浏览器直接发出CORS请求。具体来说,就是在头信息之中,增加一个Origin字段

GET /cors HTTP/1.1
Origin: http://api.bob.com
Host: api.alice.com
Accept-Language: en-US
Connection: keep-alive
User-Agent: Mozilla/5.0...

上面的头信息中,Origin字段用来说明,本次请求来自哪个源(协议 + 域名 + 端口)。服务器根据这个值,决定是否同意这次请求

简单请求回应

CORS请求设置的响应头字段,都以 Access-Control-开头:

Access-Control-Allow-Origin:必选

它的值要么是请求时Origin字段的值,要么是一个*,表示接受任意域名的请求。

Access-Control-Allow-Credentials:可选

它的值是一个布尔值,表示是否允许发送Cookie。默认情况下,Cookie不包括在CORS请求之中。设为true,即表示服务器明确许可,Cookie可以包含在请求中,一起发给服务器。这个值也只能设为true,如果服务器不要浏览器发送Cookie,删除该字段即可。

Access-Control-Expose-Headers:可选

CORS请求时,XMLHttpRequest对象的getResponseHeader()方法只能拿到6个基本字段:Cache-Control、Content-Language、Content-Type、Expires、Last-Modified、Pragma。如果想拿到其他字段,就必须在Access-Control-Expose-Headers里面指定。

非简单请求

非简单请求是那种对服务器有特殊要求的请求,比如请求方法是PUT或DELETE,或者Content-Type字段的类型是application/json。非简单请求的CORS请求,会在正式通信之前,增加一次HTTP查询请求,称为"预检"请求。

预检请求

预检"请求用的请求方法是OPTIONS,表示这个请求是用来询问的。请求头信息里面,关键字段是Origin,表示请求来自哪个源。除了Origin字段,"预检"请求的头信息包括两个特殊字段。

Access-Control-Request-Method:必选

用来列出浏览器的CORS请求会用到哪些HTTP方法,上例是PUT

Access-Control-Request-Headers:可选

该字段是一个逗号分隔的字符串,指定浏览器CORS请求会额外发送的头信息字段,上例是X-Custom-Header。

预检请求的回应

服务器收到"预检"请求以后,检查了Origin、Access-Control-Request-Method和Access-Control-Request-Headers字段以后,确认允许跨源请求,就可以做出回应。

HTTP回应中,除了关键的是Access-Control-Allow-Origin字段,其他CORS相关字段如下:

Access-Control-Allow-Methods:必选

它的值是逗号分隔的一个字符串,表明服务器支持的所有跨域请求的方法。注意,返回的是所有支持的方法,而不单是浏览器请求的那个方法。这是为了避免多次"预检"请求。

Access-Control-Allow-Headers

如果浏览器请求包括Access-Control-Request-Headers字段,则Access-Control-Allow-Headers字段是必需的。它也是一个逗号分隔的字符串,表明服务器支持的所有头信息字段,不限于浏览器在"预检"中请求的字段。

Access-Control-Allow-Credentials:可选

该字段与简单请求时的含义相同。

Access-Control-Max-Age:可选

用来指定本次预检请求的有效期,单位为秒。

总结:

字段简单请求预检请求
请求OriginOrigin
Access-Control-Request-Method
Access-Control-Request-Headers
回应Access-Control-Allow-Origin
Access-Control-Allow-Credentials
Access-Control-Expose-Headers
Access-Control-Allow-Origin
Access-Control-Allow-Methods
Access-Control-Allow-Headers
Access-Control-Allow-Credentials
Access-Control-Max-Age

node + webpack + webpack-dev-server

(1) webpack proxy

(2) 接受客户端发送的请求以后转发给其他服务器,目的是便于开发者在开发的模式下解决跨域问题

(3) 实现代理,必须有一个中间件服务器,webpack提供的中间件服务器webpack-dev-server只适用于开发阶段

// ./webpack.config.js
const path = require('path')
  devServer: {
        // host: '0.0.0.0',
        hot: true, /* 开启热点 */
        inline: true, /* 开启页面自动刷新 */
        progress: true, /* 显示打包的进度 */
        disableHostCheck: true,
        // port:8080,
        open: true,
        watchOptions: {
            aggregateTimeout: 300
        },
        proxy: {
            '/api': {
                // target: 'http://100.64.34.171:8081', /* 后台服务器地址 */
                target: 'http://127.0.0.1:8086',
                hot: true,
                ws: false,
                changeOrigin: true
                 /* pathRewrite: {
                    '^/api': '' // 这里理解成用‘/api’代替target里面的地址,后面组件中我们掉接口时直接用api代替 比如我要调用'http://40.00.100.100:3002/user/add',直接写‘/api/user/add’即可
                }*/
            }
        }
    },
  • target:表示代理到的目标地址
  • pathWrite:默认情况下,我们在/api也会被写到url中,希望删除,需要配置
  • secure默认情况下不接受转发到https服务器上,如果需要,设为false
  • changeOrigin: 它是表示是否更新代理后请求的 headers 中的 host 地址

(4)实现原理

在开发阶段,webpack-dev-server会自动启动一个本地开发服务器,我们应用在开发阶段是独立运行在localhost的一个端口上的,而后端服务器又运行在另一个地址上,由于同源策略,会出现跨域。所以使用代理,【浏览器发送请求】---> 【中间服务器】---->【服务器】; 服务器和服务器之间是不会存在跨域资源问题

window.name+ iframe跨域

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

// a.html:www.domain1.com/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);
});
// proxy.html:www.domain1.com/proxy.html
  中间代理页,与a.html同域,内容为空即可
<script>
// b.html:www.domain2.com/b.html
    window.name = 'This is domain2 data!';
</script>

location.hash + iframe跨域

实现原理: 相同的域的界面通过js直接访问,不同域的界面通过借助iframe的location.hash实现传值

 //1.html
 <iframe src="1.html" frameborder="0" id="iframe" display="none"></iframe>
    <script>
        var iframe = document.getElementById('iframe');
        //向2.html传hash
        setTimeout(function () {
            iframe.src = iframe.src + "#user=aaaa"
        }, 1000)
        //3.html的回调
        function callback(res) {
            console.log('3.html' + res)
        }
    </script>

// 2.html
 <iframe src="3.html" frameborder="0" id="iframe" display="none"></iframe>
    <script>
        var iframe = document.getElementById('iframe');
        //监听1.html传递的hash,传给3.html
        window.onhashchange = function () {
            iframe.src = iframe.src + location.hash
        }
    </script>
//3.html
  <script>
        //监听2.html传递的hash
        window.onhashchange = function () {
            window.parent.parent.callback('didi' + location.hash.replace('#user=', ''))
        }
    </script>

document.domain+ iframe跨域

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


  //from页面中
     <iframe src="下一级页面地址" frameborder="0" id="iframe"></iframe>
    <script>
        document.domain = 'aa.com'
        var user = 'admin'
    </script>
  //to页面中
     <script>
        document.domain = 'aa.com'
        console.log('获取上一级页面中的变量' + window.parent.user)
    </script>

webSocket

WebSocket是一种网络通信协议,它实现了浏览器与服务器全双工通信,同时允许跨域通讯。

思考💡:设置了 Access-Control-Allow-Credentials和withCredentials为什么cookie还是没能成功跨域?

withCredentials:表示XHR是否接收cookies和发送cookies

浏览器端要跨域 服务器端要检查 ,最重要的是Access-Control-Allow-Origin,标识允许哪个域的请求

(1) Access-Control-Allow-Origin:*:即使设置了withCredentials ,浏览器也不会发送cookies,因为这意味着把cookies开放给所有网站 假设当前是A网站,并且在cookie里写入了身份凭证 用户同时打开了B网站, 那么B网站给A网站的服务器发的所有请求都是以A用户的身份进行的, 这将导致CSRF问题

(2) Access-Control-Allow-Origin:xxx指定域

参考

9种常见的前端跨域解决方案(详解)

webpack(六)——webpack 解决跨域的原理

没错,就是Access-Control-Allow-Origin,跨域

什么是跨域?如何实现?

JSONP原理及实现

前端多种跨域方式实现原理详解