常用的跨域解决方案

794 阅读8分钟

跨域产生的原因:

浏览器的同源策略,同源策略是浏览器最核心也是最基本的安全功能,不同源的客户端脚本在没有明确授权的情况下,不能读写对方资源。

跨域产生的判断:

当前页面的URL地址 和 请求数据接口的地址: 如果 协议、域名、端口号 都一致,则为同源策略; 三者中只要有一个不一样,就是跨域请求

不受同源策略限制的:

  1. 页面中的链接,重定向以及表单提交是不会受到同源策略限制的。
  2. 跨域资源的引入是可以的。但是js不能读写加载的内容。如嵌入到页面中的script,img,link,iframe等。
真实项目中,其实跨域策略比同源策略还要多一些:
  1. 现在的项目一般都是前后端分离,所以项目部署的时候,一般也是分开部署的(不排除后期有部署在一起的) => 有些公司需要web服务器由前端开发部署 linux + nginx...

  2. 为了保证服务器资源的合理利用,我们一般把服务器分成几类(一个项目,它访问的资源是分门别类在不同服务器上的)

    • WEB服务器(页面、css、js等资源)
    • 图片服务器
    • 音视频服务器
    • 数据服务器 ...
  3. 一个项目太大,我们分散成为很多子项目(基于二级域名分别部署),但是所有的子项目之间的数据需要互相访问(内部联调),此时也是以跨域为主

跨域请求的解决方案:

很早有这样一种方案(开发环境下):

    项目部署的时候肯定会在一起(项目一旦部署不存在跨域访问),但是在开发的时候,开发者需要让本地的项目也能访问到正常的数据接口 

    1)最开始需要前端开发把后台代码也在本地部署起来,和本地的WEB部署在一起,在本地也产生同源的环境

    缺点:需要前端随时同步后台的代码到本地,而且本地也要安装一些后台的环境

    2)本地无需拿后台代码等,本地启动一个WEB服务,通过修改本地的HOST文件,模拟出和数据服务器相同的环境 xampp / wampp

其他方案总结:
  1. JSONP
  2. 其它一些方案(IFRAME
    • window.name
    • window.location.hash
    • document.domain 处理主域相同,子域不同的相互请求
    • postMessage(H5)
  3. CORS 跨域资源共享
    • 原理:让服务器端允许客户端跨域AJAX请求(基本上靠服务端设置允许跨域,客户端无需做太多的修改,直接正常发送AJAX请求即可)
  4. webpack的崛起,带动了http proxy这种方案的兴起
    • 原理:利用webpack-dev-server插件,构建本地服务AA(预览本地开发的项目),我们发送AJAX请求,都先把请求发送给AA,有AA帮我们在发送给真正的服务器,AA起了一个中间代理的作用,从而解决跨域的问题!
    • 缺点:但是上述操作是基于AA这个服务的(webpack-dev-server),项目最后部署的时候,没有webpack-dev-server插件,此时我们需要基于其它方案(例如:nginx反向代理)实现出当初AA服务代理的作用才可以!
  5. nginx反向代理
  6. web scoket
一、 JSONP:利用了SCRIPT(IMG/IFRAME)等标签不存在跨域请求限制的特点,实现跨域请求的
  • 只能是GET请求(不能处理POST请求)
  • =>传递给服务器的信息都是基于问号传参的方式传递过去的
  • =>为了能够拿到服务器返回的结果,需要利用回调函数的机制,把客户端的某个函数(全局)名传递给服务器,由服务器接收到函数后进行特殊的处理 ?callback=func (callback这个名字是和服务器商定好的,一般都叫callback这个名字)
  • JSONP的服务器端处理:所有的JSONP请求都需要服务器端接收到请求后做特殊处理
// 页面:
function func(result) { // 全局函数
	console.log(result);  
	// result => {code: 0, codeText: 'SUCCESS', text: 'LX:1 === NAME:zf'}
}
// 请求结束,客户端拿到一个这样的结果:func({....}),浏览器会把函数执行(之所以这个函数在传递的时候需要是全局函数,因为后期获取到结果的时候,需要把它执行,只有全局函数才能在任何地方获取到)
let script = document.createElement('script');
script.src = `http://127.0.0.1:1001/test?lx=1&name=zf&callback=func&_=${new Date().getTime()}`;
document.body.appendChild(script);
              
// 后台(node):
app.get('/test', (req, res) => {
	let {
		lx,
		name,
		callback //callback存储的就是客户端传递过来的函数名
	} = req.query;
	let data = {
		code: 0,
		codeText: 'SUCCESS',
		text: `LX:${lx} === NAME:${name}`
	};
	//特殊处理:把客户端传递的函数名和需要给客户端的数据,拼成这样的格式 “函数(数据)”
	res.send(`${callback}(${JSON.stringify(data)})`);
});
二、 iframe的window.name

A(父) 嵌套 B(子)只能通过window.name传递数据,取数据,无法获取B里面的dom元素
A页面(WEB):http://127.0.0.1:1001/NAME/A.html
B页面(API):http://127.0.0.1:1002/NAME/B.html
proxy:临时过渡页面,不需要做任何处理,需与A相同的源下

// A页面:
let count = 0;
let iframe = document.createElement('iframe');
iframe.style.display = 'none';
iframe.src = 'http://127.0.0.1:1002/NAME/B.html';
iframe.onload = function () {
	// 想要正常拿到B中的内容,还需要把IFRAME的指向重新指回和A相同的源下
	// proxy临时过渡页面,不需要做任何的处理
	if (count === 0) {
		iframe.src = '/NAME/proxy.html';
		count++;
		return;
	}
	// iframe.contentWindow.document获取的是proxy页面的dom元素,而非 B页面
	console.log(iframe.contentWindow.name); // {code:0, codeText:'MESSAGE', data:'传递的信息'}
};
document.body.appendChild(iframe);


// B页面:
// 服务器端需要返回给A的信息都在window.name中存储着
window.name = JSON.stringify({
	code: 0,
	codeText: 'MESSAGE',
	data: '传递的信息'
});

三、window.postMessage()

MDN链接:developer.mozilla.org/zh-CN/docs/…

  • otherWindow.postMessage(message, targetOrigin, [transfer]);分发一个messageEvent消息; 将消息事件对象暴露给接收消息的窗口
        targetOrigin: 指定哪些窗口可以接收到消息事件;① *:无限制,将导致数据泄露(不推荐) ② 确切的targetOrigin
  • window.addEventListener("message", receiveMessage, false);监听分发的message
        receiveMessage函数 ev.data其他窗口传递过来的对象 ev.source发送消息事件的窗口对象
// A.html
<iframe id="iframe" src="http://127.0.0.1:1002/MESSAGE/B.html" frameborder="0" style="display: none;"></iframe>
// 向服务器端B发送请求
iframe.onload = function () {
	// 传递给对应的服务器内容 => '传递的信息'
	iframe.contentWindow.postMessage('传递的信息', 'http://127.0.0.1:1002/');
};

//=>监听服务端B返回的信息
window.onmessage = function (ev) {
	console.log(ev.data); // {code:0, codeText:'OK',data:'传递的信息@@@'}
};

// B.html
// 监听客户端A发送过来的请求信息,把数据返回给客户端A
window.onmessage = function (ev) {
	console.log(ev,'B');
	//=>ev.data:客户端A发送的信息
	//=>ev.source:客户端A
	ev.source.postMessage(JSON.stringify({
		code: 0,
		codeText: 'OK',
		data: ev.data + '@@@'
	}), '*');
};
四、CORS跨域资源共享

MDN链接:developer.mozilla.org/zh-CN/docs/…

1.允许的源ORIGIN有两种写法:

  • "*" :允许所有客户端发送请求,但是不能携带资源凭证(Credentials===false)
  • "xxx":想要携带资源凭证,只能指定一个源,不能指定多个

2.POST请求中,在请求发送前,一般都会先发送一个OPTIONS试探请求(预检请求 preflight request),目的是验证,当前请求能否和服务器端建立链接,能链接上在发正式请求

// node后台
app.use((req, res, next) => {
	res.header("Access-Control-Allow-Origin", "http://127.0.0.1:5500");
	res.header("Access-Control-Allow-Credentials", true);
	res.header("Access-Control-Allow-Headers", "Content-Type,Content-Length,Authorization");
	res.header("Access-Control-Allow-Methods", "PUT,POST,GET,DELETE,OPTIONS,HEAD");
	req.method === 'OPTIONS' ? res.send('CURRENT SERVICES SUPPORT CROSS DOMAIN REQUESTS!') : next();
});
五、webpack的devServer插件(仅适用于开发环境, 项目部署时没有该插件)

项目启动:http://localhost:1001    接口:http://127.0.0.1:1002/test

// webpack.config.js/vue.config.js:
module.export = {
    devServer: {
        post: 1001, // 项目的端口为1001
        proxy: { // 启动代理
            '/api': {  // 匹配http://127.0.0.1:1002的所有/api接口
                target: 'http://127.0.0.1:1002', //服务器的地址
                sercure: false, //若为true则表示以https开头
                pathRewrite: {'^/api': ''}, //重写地址,正则匹配,把请求地址/api/user的/api去掉,实际接口为/user
                changeOrigin: true //把请求头当中的host改成服务器的地址
            }
        }
    }
}

// 页面请求
// 请求路径必须是相对路径,不能是绝对路径
axios.get('/api/user').then(res=>{})
六、gulp的http-proxy-middleware插件(跟webpack的devServer插件一样,仅适用于开发环境)
// package.json代码:
{
  "devDependencies": {
    "gulp": "^4.0.2",
    "gulp-connect": "^5.7.0",
    "http-proxy-middleware": "^0.19.1"
  }
}
// gulpfile.js代码:
var gulp = require('gulp'),
    connect = require('gulp-connect'),
    proxy = require('http-proxy-middleware');   

//代理
gulp.task('server', function() {
    connect.server({
        livereload: true,
        port: 1001,
        open: true,
        root: './',
        host: '0.0.0.0',
        middleware: function(connect, opt) {
            return [
                proxy('/api', {
                    target: 'http://127.0.0.1:1002',
                    changeOrigin: true,
                    pathRewrite: {
                        '^/api': ''
                    }
                })
            ]
        }
    })
});
七、nginx服务器反向代理

参考资料:
www.cnblogs.com/ysocean/p/9… segmentfault.com/a/119000001…
www.php.cn/nginx/

反向代理,其实客户端对代理是无感知的,因为客户端不需要任何配置就可以访问,我们只需要将请求发送到反向代理服务器,由反向代理服务器去选择目标服务器获取数据后,在返回给客户端,此时反向代理服务器和目标服务器对外就是一个服务器,暴露的是代理服务器地址,隐藏了真实服务器IP地址。

nginx 和 Apache 一样是服务器
项目启动(nginx服务器):http://localhost:8060/
接口:http://127.0.0.1:1002/api/test

// nginx.config
http {
    server {
        listen        8060;  // nginx启动的端口,
        server_name   localhost; // nginx启动的域名
        location / {
            root "F:/example"; // 项目的文件路径
            add_header Access-Control-Allow-Origin *; // 接受所有请求源
            add_header Access-Control-Allow-Methods 'GET, POST, OPTIONS';  // 发送预检请求(preflight request)需要用到 OPTIONS方法
            add_header Access-Control-Allow-Headers 'DNT,X-Mx-ReqToken,Keep-Alive,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Authorization';

        }
        location /api {    // 匹配所有/api的接口    
            // 被代理服务器的地址(即接口地址,若项目有端口号,不要忘记加端口号)
            proxy_pass  http://127.0.0.1:1002; 
        }
    }
}

// 页面请求
// 请求路径必须是相对路径,不能是绝对路径
axios.get('/test').then(res => {
    console.log(res)
})

请求的信息:

八、谷歌浏览器(仅适用于开发环境)

在浏览器的属性目标后面设置--args --disable-web-security --user-data-dir,路径后面要有个空格

谷歌更新到最新版本(81)后disable-web-security --user-data-dir失效,最新版本的谷歌 --user-data-dir 需要携带参数 就是打开当前目录的路径,通过加上--args --disable-web-security --user-data-dir="/tmp/chromedevtest"即可