「深入浅出」前端开发中常用的几种跨域解决方案
编者荐语
本文将为大家介绍,前端开发中,最常用的几种跨域解决方案;
看完本文可以系统地掌握,不同种跨域解决方案间的巧妙,以及它们的用法、原理、局限性和适用的场景
包括以下几个方面:
跨域的现象,和几种常见的跨域表现 跨域的解决方案(原理分析) 修改本地HOST JSONP CORS Proxy Nginx反向代理 Post Message(利用 iframe
标签,实现不同域的关联)
同源是什么?
如果两个URL的协议protocol
、主机名host
和端口号port
都相同的话,则这两个URL是同源。
同源策略
同源策略是一个重要的安全策略。它能够阻断恶意文档,减少被攻击的媒介。
真实项目中,很少有同源策略,大部分都是非同源策略
跨域是什么?
当协议、域名与端口号中任意一个不相同时,都算作不同域,不同域之间相互请求资源的表现(非同源策略请求),称作”跨域“。
跨域现象
那么我们就下面的网址分析一下,哪一块是协议,哪一块是域名及端口号
http://kbs.sports.qq.com/index.html
协议:http(还有一种https协议)
域名:kbs.sports.qq.com
端口号:80
https://127.0.0.1:3000
协议:https
域名:127.0.0.1
端口号:3000
假如我们的真实项目开发中的Web服务器
地址为 ”http://kbs.sports.qq.com/index.html“,而需要请求的数据接口
地址为 "http://api.sports.qq.com/list"。
当Web服务器的地址向数据接口的地址发送请求时,便会造成了跨域现象
造成跨域的几种常见表现
服务器分开部署(Web服务器 + 数据请求服务器) 本地开发(本地预览项目 调取 测试服务器的数据) 调取第三方平台的接口
Web服务器:主要用来静态资源文件的处理
解决方案
修改本地HOST(不作介绍) JSONP CORS Proxy Nginx反向代理 Post Message(利用 iframe
标签,实现不同域的关联)
在后面会详细分析这四种解决方案的原理和用法配置,以及它们的优点和局限性
注意: 基于
ajax
或fetch
发送请求时,如果是跨域的,则浏览器默认的安全策略会禁止该跨域请求
补充说明:以下所有的测试用例,均由
Web:http://127.0.0.1:5500/index.html
向API:http://127.0.0.1:1001/list
发起请求
API接口的服务器端是自己通过express
建立的,下文在服务器端以app.use
中间件的形式接受来自客户端
的请求并做处理。
即 在“http://127.0.0.1:1001/list”from origin“http://127.0.0.1:55”上对XMLHttpRequest的访问已被CORS策略阻止:被请求的资源上没有“Access- control - allow-origin”头
在后端开启了一个端口号为1001的服务器之后,我们来实践一下
let xhr = new XMLHttpRequest;
xhr.open('get', 'http://127.0.0.1:1001/list');
xhr.onreadystatechange = () => {
if (xhr.status === 200 && xhr.readyState === 4) {
console.log(xhr.responseText);
}
};
xhr.send();
这就是由于浏览器默认的安全策略禁止导致的。
下面介绍一下几种常见的解决方案。
JSONP
原理:JSONP
利用script
标签不存在域的限制,且定义一个全局执行上下文
中的函数func
(用来接收服务器端返回的数据信息)来接收数据,从而实现跨域请求。
弊端:
只允许GET请求 不安全:只要浏览器支持,且存在浏览器的全局变量里,则谁都可以调用
图解JSONP的原理
手动封装JSONP
callback
必须是一个全局上下文中的函数(防止不是全局的函数,我们需要把这个函数放在全局上,并且从服务器端接收回信息时,要浏览器执行该函数)
注意:
uniqueName
变量存储全局的回调函数(确保每次的callback
都具有唯一性)检验 url
中是否含有"?",有的话直接拼接callback
,没有的话补”?“
// 客户端
function jsonp(url, callback) {
// 把传递的回调函数挂载到全局上
let uniqueName = `jsonp${new Date().getTime()}`;
// 套了一层 anonymous function
// 目的让 返回的callback执行且删除创建的标签
window[uniqueName] = data => {
// 从服务器获取结果并让浏览器执行callback
document.body.removeChild(script);
delete window[uniqueName];
callback && callback(data);
}
// 处理URL
url += `${url.includes('?')} ? '&' : '?}callback=${uniqueName}'`;
// 发送请求
let script = document.createElement('script');
script.src = url;
document.body.appendChild(script);
}
// 执行第二个参数 callback,获取数据
jsonp('http://127.0.0.1:1001/list?userName="lsh"', (result) => {
console.log(result);
})
// 服务器端
// Api请求数据
app.get('/list', (req, res) => {
// req.query 问号后面传递的参数信息
// 此时的callback 为传递过来的函数名字 (uniqueName)
let { callback } = req.query;
// 准备返回的数据(字符串)
let res = { code: 0, data: [10,20] };
let str = `${callback}($(JSON.stringify(res)))`;
// 返回给客户端数据
res.send(str);
})
测试用例展示:
客户端请求的 url
服务端返回的数据 返回的 callback
返回的数据信息 result
// 服务器请求的 url
Request URL:
http://127.0.0.1:1001/list?userName="lsh"&callback=jsonp159876002
// 服务器返回的函数callback
jsonp159876002({"code":0, "data": [10,20]});
// 客户端接收的数据信息
{ code: 0, data: Array(2) }
当浏览器发现返回的是jsonp159876002({"code":0, "data": [10,20]});
这个函数,会自动帮我们执行的。
JSONP弊端
在上文中说到只要服务器端那里设置了允许通过jsonp
的形式跨域请求,我们就可以取回数据。
下面是在我们封装完jsonp
方法之后,向一个允许任何源向该服务器发送请求的网址xxx
jsonp('https://matchweb.sports.qq.com/matchUnion/cateColumns?from=pc', result => {
console.log(result);
});
CORS
上文提到,不允许跨域的根本原因是因为Access-Control-Allow-Origin
已被禁止
那么只要让服务器端设置允许源
就可以了
原理:解决掉浏览器的默认安全策略,在服务器端设置允许哪些源请求就可以了
先来看一下下面的设置有哪些问题
// 服务器端
app.use((req, res, next) => {
// * 允许所有源(不安全/不能携带资源凭证)
res.header("Access-Control-Allow-Origin", "*");
res.header("Access-Control-Allow-Credentials", true);
/* res.header("Access-Control-Allow-Headers", "Content-Type,....");
res.header("Access-Control-Allow-Methods", "GET,..."); */
// 试探请求:在CORS跨域请求中,首先浏览器会自己发送一个试探请求,验证是否可以和服务器跨域通信,服务器返回200,则浏览器继续发送真实的请求
req.method === 'OPTIONS' ? res.send('CURRENT SERVICES SUPPORT CROSS DOMAIN REQUESTS!') : next();
});
// 客户端
let xhr = new XMLHttpRequest;
xhr.open('get', 'http://127.0.0.1:1001/list');
xhr.setRequestHeader('Cookie', 'name=jason');
xhr.withCredentials = true;
xhr.onreadystatechange = () => {
if (xhr.status === 200 && xhr.readyState === 4) {
console.log(xhr.responseText);
}
};
xhr.send();
当我们一旦在服务器端设置了允许任何源
可以请求之后,其实请求是不安全的,并且要求客户端
不能携带资源凭证(比如上文中的Cookie字段),浏览器端会报错。
告诉我们Cookie
字段是不安全的也不能被设置的,如果允许源为'*'
的话也是不允许的。
假如在我们的真实项目开发中
正确写法✅
设置单一源(安全/也可以携带资源凭证/只能是单一一个源) 也可以动态设置多个源:每一次请求都会走这个中间件,我们首先设置一个白名单,如果当前客户端请求的源在白名单中,我们把 Allow-Origin
动态设置为当前这个源
app.use((req, res, next) => {
// 也可自定义白名单,检验请求的源是否在白名单里,动态设置
/* let safeList = [
"http://127.0.0.1:5500",
xxx,
xxxxx,
]; */
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,....");
res.header("Access-Control-Allow-Methods", "GET,..."); */
// 试探请求:在CORS跨域请求中,首先浏览器会自己发送一个试探请求,验证是否可以和服务器跨域通信,服务器返回200,则浏览器继续发送真实的请求
req.method === 'OPTIONS' ? res.send('CURRENT SERVICES SUPPORT CROSS DOMAIN REQUESTS!') : next();
});
CORS的好处
原理简单,容易配置,允许携带资源凭证 仍可以用 ajax
作为资源请求的方式可以动态设置多个源,通过判断,将 Allow-Origin
设置为当前源
CORS的局限性
只允许某一个源发起请求 如多个源,还需要动态判断
Proxy
Proxy翻译为“代理”,是由webpack配置的一个插件,叫"webpack-dev-server"(只能在开发环境中使用)
Proxy在webpack中的配置
const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');
module.exports = {
mode: 'production',
entry: './src/main.js',
output: {...},
devServer: {
port: '3000',
compress: true,
open: true,
hot: true,
proxy: {
'/': {
target: 'http://127.0.0.1:3001',
changeOrigin: true
}
}
},
// 配置WEBPACK的插件
plugins: [
new HtmlWebpackPlugin({
template: `./public/index.html`,
filename: `index.html`
})
]
};
图解Proxy的原理
Proxy代理其实相当于由webpack-dev-server
配置在本地创建了一个port=3000的服务,利用node
的中间层代理(分发)解决了浏览器的同源策略限制。
但是它只能在开发环境下使用,因为dev-server
只是一个webpack
的一个插件;
如果需要在生产环境下使用,需要我们配置Nginx
反向代理服务器;
另外如果是自己实现node服务层代理
:无论是开发环境还是生产环境都可以处理(node中间层和客户端是同源,中间层帮助我们向服务器请求数据,再把数据返回给客户端)
Proxy的局限性
只能在本地开发阶段使用
配置Nginx反向代理
主要作为生产环境下跨域的解决方案。
原理:利用Node中间层的分发机制,将请求的URL转向服务器端的地址
配置反向代理
server {
listen: 80;
server_name: 192.168.161.189;
loaction: {
proxy_pass_http://127.0.0.1:1001; // 请求转向这个URL地址,服务器地址
root html;
index index.html;
}
}
简单写了一下伪代码,实际开发中根据需求来配。
POST MESSAGE
假设现在有两个页面,分别为A页面port=1001
、B页面port=1002
,实现页面A与页面B的页面通信(跨域)
原理:
把 B页面当做A的子页面嵌入到A页面里,通过 iframe.contentWindow.postMessage
向B页面传递某些信息在A页面中通过 window.onmessage
获取A页面传递过来的信息ev.data
(见下代码)同理在B页面中通过 ev.source.postMessage
向A页面传递信息在A页面中通过 window.onmessage
获取B页面传递的信息
主要利用内置的postMessage
和onmessage
传递信息和接收信息。
A.html
// 把 B页面当做A的子页面嵌入到A页面里
B.html