细解跨域以及跨域的解决方案
ztj100 2024-12-04 17:11 49 浏览 0 评论
跨域,对于正在学习或者已经就业的前端同学而言,就是老朋友。只要涉及“请求”“前后端交互”“开发阶段”等关键字,都避不开跨域。同时它也是面试中最常出现的考点之一,面试官可以通过跨域,了解应聘者对网络协议、网络安全等概念的理解。
跨域并不是阻碍前后端交互的障碍,什么是跨域,怎么避开跨域带来的不便,本文主要细解三种主流的解决方案:JSONP,CORS,代理服务器,细致地解开跨域相关的迷惑。
一、同源策略
同源策略是一个重要的安全策略,它用于限制一个Origin的文档或者它加载的脚本如何能与另一个源的资源进行交互。它能帮助阻隔恶意文档,减少可能被攻击的媒介。
Origin:指web文档的来源,Web 内容的来源取决于访问的URL的方案 (协议),主机 (域名) 和端口定义。只有当方案,主机和端口都匹配时,两个对象具有相同的起源。
二、跨域
关于URL是否同源,根据上图中的①②③进行判断即可,只要有一点不同,就达到跨域的条件。顺带一提,即便是向域名对应的ip进行资源请求,仍然会跨域。
IE的特殊性:Internet Explorer 的同源策略有两点差异,一是IE未将端口号纳入同源策略的检查,其次是两个高度互信的域名也不受同源策略的检查。
常见的跨域情景:
浏览器内常见的跨域报错:
跨域出现的场景:
一般常见于开发阶段,本地启动项目后,当前页面域名和后台服务器域名不相同,导致跨域。在项目上线后,会通过统一域名、后端配置域名白名单等方式避免跨域。
下方的解决方案中,我们通过koa2框架搭建服务器,实现一系列的情景模拟。
三、跨域的解决方案
1.JSONP
原理:通过script标签没有跨域限制的特性,进行资源的请求和获取。
限制:需要目标服务器进行配合,且仅支持get请求
我们直接通过代码和注释,理解jsonp的使用前端代码如下:
<script>
window.jsonp = function(res){
console.log(res);
}
</script>
<script src="http://localhost:9527/jsonp?val=123&cb=jsonp"></script>
后端代码如下:
// 定义jsonp接口
router.get('/jsonp', async (ctx, next) => {
/*
1.后端通过query获取前端传来的请求参数
其中包括:
· 交予后端进行功能逻辑操作的数据,如val
· 交予后端进行jsonp操作的函数名,如cb
*/
const {cb, val} = ctx.query
// 2.调用回调函数,进行传参,将处理好的数据返回给前端
if(val === '123'){
const requestData = {
code: 10001,
data: '登陆成功'
}
//在响应体中触发目标函数,并将处理好的数据requestData作为实参传入
ctx.body = `${cb}(${JSON.stringify(requestData)})`;
}
})
前端通过window对象,在全局挂载了一个待触发的函数。
后端通过响应体触发这个函数,并将数据作为入参,传给前端。
了解简单的实现后,前端可以对jsonp的功能再进行一层封装:
/*
1. 生成script标签,我们需要script标签进行接口的调用
2. 处理参数数据,分别整理好接口,接口参数,函数名等数据,并进行填充
3. 写入生成好的script标签,实现接口的调用(返回promise对象,便于链式调用)
4. 清除script标签
*/
function jsonp(requestData) {
// 对传入参数进行处理
const { url, data, jsonp } = requestData;
let query = '';
for (let key in data) {
query += `${key}=${data[key]}&`;
}
const src = `${url}?${query}jsonp=${jsonp}`;
// 生成、填充script标签,在页面中挂载调用接口
let scriptTag = document.createElement('script');
scriptTag.src = src;
document.body.appendChild(scriptTag);
return new Promise((resolve, reject) => {
window[jsonp] = function(rest){
resolve(rest)
document.body.removeChild(scriptTag)
}
})
}
// 整理数据
const requestData = {
url: 'http://localhost:9527/jsonp',
data: {
val: 123,
},
jsonp: 'getMessage'
}
// 接口调用
btn.onclick = function () {
jsonp(requestData).then(function (response) {
console.log(response);
})
}
2.CORS
Cross-Origin Resource sharing(跨域资源共享),是一种基于HTTP头的机制,该机制允许服务器标示除了它自己以外其他origin(域名,协议和端口),既浏览器在跨域的情景下仍然能从目标服务器请求并获取资源。
而对服务器数据可能产生副作用的HTTP请求方法,都会触发CORS中的预检机制。
CORS中通过预检机制(preflight request)检查服务器是否允许浏览器发送真实请求,浏览器会先发送一个预检请求(option请求),请求中会携带真实请求的请求信息:
origin:请求的来源
Access-Control-Request-Method:
通知服务器在真正的请求中会采用哪种HTTP方法(GET,POST,DELETE...)
Access-Control-Request-Headers:通知服务器在真正的请求中会采用哪些请求头
服务器可以在预检请求中,可以根据以上三条信息,确定预检请求是否通过:
//server.js
app.use(async (ctx, next) => {
// 允许跨域资源共享的白名单
const whiteList = ['http://127.0.0.1:5500']
// 判断目标源是否通行
const pass = whiteList.includes(ctx.header.origin)
// 对于预检请求,如果没有设置正确的响应状态,浏览器会直接拦截真实请求,直接报错提示跨域
// 所以我们可以在这一部分,确定客户端的请求是否符合我们的要求
if (ctx.method === "OPTIONS") {
if (!pass) return
// 预检放行
ctx.status = 204
}
await next();
});
响应的状态码是决定预检请求是否通过的关键,返回正常的状态码(通常是204)就能通过预检请求,让浏览器发出真实的请求。
在代码中也可以看出,pass是决定预检请求的关键,那在实际的项目中,还得根据设计去决定通行的具体条件。当通过预检请求后,后台可以设置对应的响应头数据,例如是否允许目标源跨域资源共享:
//server.js
app.use(async (ctx, next) => {
console.log('middleware for cors');
// 允许跨域资源共享的白名单
const whiteList = ['http://127.0.0.1:5500']
// 判断目标源是否通行
const pass = whiteList.includes(ctx.header.origin)
// 对于预检请求,如果没有设置正确的响应状态,浏览器会直接拦截真实请求,直接报错跨域
// 所以我们可以在这一部分,确定客户端的请求是否符合我们的要求
if (ctx.method === "OPTIONS") {
if (!pass) return
// 预检放行
ctx.status = 204
}
// 允许访问的origin
ctx.set("Access-Control-Allow-Origin", ctx.headers.origin);
// cookie是否允许携带
ctx.set("Access-Control-Allow-Credentials", true);
// 允许访问的HTTP方法
ctx.set("Access-Control-Request-Method", "PUT,POST,GET,DELETE,OPTIONS");
// 哪些请求头允许通行
ctx.set(
"Access-Control-Allow-Headers",
"X-Requested-With,Content-Type,Accept,Origin"
);
// 暴露给客户端的响应头信息,在不设置的情况下,客户端只能获取默认的响应头,如’content-type‘
ctx.set(
"Access-Control-Expose-Headers",
"With-Requested-Key"
);
// 设置对应的响应头数据
ctx.set(
"With-Requested-Key",
"HW"
);
// 预检结果的缓存时间,毫秒为单位,Firefox上限是86400-24小时,Chromium(谷歌引擎)上限是7200-2小时
ctx.set("Access-Control-Max-Age", 0);
await next();
});
其中需要注意两个点:
关于Access-Control-Expose-Header
使用CORS时,浏览器只允许获取默认的响应头,像上文代码中的标头With-Requested-Key,即便我们可以通过浏览器的调试器查看,也无法通过代码去获取,这时候就需要后台通过Access-Control-Expose-Header进行暴露(后台代码在已在上方统一贴出)。
前端代码
<body>
<button id="btn"> 请求资源 </button>
</body>
<script>
btn.onclick = function () {
axios.post('http://localhost:9527/getMessage', {
firstName: 'Fred',
lastName: 'Flintstone'
})
.then(function (response) {
// 可以在里面查找到暴露出来的响应头数据,如’With-Requested-Key‘: "HW"
console.log(response.headers);
})
.catch(function (error) {
console.log(error);
});
}
</script>
关于Access-Control-Allow-Credentials
使用CORS时,默认不携带cookie,需要同时满足三个条件,才能在使用CORS时进行cookie的传递:
浏览器的请求中,设置withCredentials参数为true
服务端设置标头Access-Control-Allow-Credentials为true
服务端设置标头Access-Control-Allow-Origin不为*
我们可以在原生ajax请求中设置该参数,或者在axios的默认配置中设置该参数:
// 原生ajax
const xhr = new XMLHttpRequest()
xhr.withCredentials = true
// axios
axios.defaults.withCredentials = true;
Ok,明白CORS的作用,以及明白CORS中的预检机制后,接下来是了解什么时机下会触发预检机制。
CORS中归纳了一系列不会触发预检机制的请求场景,即满足所有下述条件的情况下,统称为简单请求:
使用这三种方法之一:GET HEAD POST
不得人为设置此集合外的其他首部字段:Accept Accept-Language Content-Language Content-Type
Content-type的值仅限于这三者之一:
text/plain
multipart/form-data
application/x-www/form-urlencoded
请求中,XMLHttpRequest实例没有注册任何事件监听器,即XMLHttpRequest实例对象可以使用XMLHttpRequest.upload属性进行访问
请求中没有使用ReadableStream对象
小结:CORS中主要区分了简单请求和复杂请求两种情况,复杂请求会触发CORS的预检机制。通过上方的案例,也可以清楚CORS的配置主要是在服务端,但客户端也需要知道CORS的使用注意点,例如响应头数据的获取以及cookies的携带配置,这些知识应该是前后端都需要掌握的技能点。
3.服务器代理
同源策略主要是限制浏览器和服务器之间的请求,服务器与服务器之间并不存在跨域。
我们可以通过koa2模拟和实现这种概念:
//前端代码
<body>
<button id="btn"> 请求资源 </button>
<script>
btn.onclick = function () {
let url = checkUrlProxy('http://localhost:9527/api/getMessage','api')
axios.post(url, {
firstName: 'Fred',
lastName: 'Flintstone'
})
.then(function (response) {
console.log(response);
})
}
// 判断接口是否携带api字段,若是,则更改为代理服务器对应的域名
function checkUrlProxy(url, proxyFlag){
let proxyServer = 'http://localhost:1005'
let urlArr = [url.split('/')[1],url.split('/')[3]]
if(urlArr.includes(proxyFlag)) {
return `${proxyServer}/${proxyFlag}${url.split(proxyFlag)[1]}`
}
return url
}
//
</script>
</body>
前端的代码部分,通过checkUrlProxy函数简单地确定本次请求是否要转向代理服务器。
后端代码如下:
//proxyServer.js
let requestFlag = false
let body = ''
app.use(async (ctx, next) => {
// 全放行
if (ctx.method === "OPTIONS") {
ctx.status = 204
requestFlag = false
} else {
requestFlag = true
}
ctx.set("Access-Control-Allow-Origin", "*");
ctx.set("Access-Control-Allow-Credentials", true);
ctx.set("Access-Control-Request-Method", "*");
ctx.set(
"Access-Control-Allow-Headers",
"X-Requested-With,Content-Type,Accept,Origin"
);
ctx.set("Access-Control-Max-Age", 86400);
// 根据具体情况进行修改
ctx.set("Access-Control-Expose-Headers", "With-Requested-Key");
await next();
if(requestFlag) {
ctx.body = body
body = ''
}
});
app.use(async (ctx, next) => {
if (!requestFlag) return
await p4r(ctx)
});
function p4r(ctx) {
return new Promise((res, rej) => {
const proxyRequest = http.request({
host: '127.0.0.1',
port: 9527,
path: ctx.url,
method: ctx.method,
headers: ctx.header
},
serverResponse => {
serverResponse.on('data', chunk => {
body += chunk
})
serverResponse.on('end', () => {
res(body)
})
}
)
proxyRequest.end()
})
}
app.on('error', (err, ctx) => {
console.error('server error', err, ctx)
});
app.listen(1005, (err) => {
if (err) console.log('服务器启动失败');
else console.log('proxy server 1005 running --> ???');
})
//targetServer.js
const data = {val : 123}
// 配合代理服务器的post路由
router.post('/api/getMessage', (ctx) => {
ctx.body = JSON.stringify(data)
})
// 定义好路由组件的内容后进行路由注册
app.use(router.routes())
app.on('error', (err, ctx) => {
console.error('server error', err, ctx)
});
app.listen(9527, (err) => {
if (err) console.log('服务器启动失败');
else console.log('服务器启动成功');
})
后端代码主要分两部分:
代理服务器(proxyServer),代理服务器设置CORS时不限制通行,在koa2框架中,通过中间件向目标服务器发送请求,当接收到对应数据后,再响应给浏览器
目标服务器(targetServer),目标服务器不需要做太复杂的配置,案例中只是将数据传递给请求方
Ok,我们通过这个案例,明确代理服务器的具体效果,浏览器向目标服务器直接请求资源,仍然会受到同源策略的影响,但通过代理服务器向目标服务器请求资源时,却没这种限制。
那在实际项目中,我们可以通过脚手架或打包工具的配置文件,简洁方便地设置代理服务器,无需自己手写服务器代码,拿vue的脚手架为例:
devServer:{
proxy:{
'api':{
target:'127.0.0.1:9527', //目标服务器地址
changeOrigin: true, // 是否允许跨域
pathRewrite: { //是否重写接口
'api':'',
}
}
}
}
在配置的时候,可以通过框架的脚手架,或者打包工具确定配置文件,例如一些熟悉的字眼:vue.config.jswebpack.config.jspackage.json(react),更准确的做法就是直接去对应工具的官方文档查阅代理服务器的配置介绍。
总结
对于跨域,许多同学都答得上来跨域是怎么产生的,以及解决跨域的方案。但在交流过程中,就总是一两句就讲完让我觉得有点可惜。
前后端交互,或者应该说网络协议,一直都是个大课题,是只要涉及这一块的程序员,都应该而且有必要学习的内容。类似上文中CORS配置时前后端要如何配合,以及使用CORS时前端的注意点都少有人提及。后端是主要的配置方,但不代表这一块的知识限于只需后端理解。
了解知识点的本质,才能尽量保证在不同的项目场景实施对应方案。
相关推荐
- 其实TensorFlow真的很水无非就这30篇熬夜练
-
好的!以下是TensorFlow需要掌握的核心内容,用列表形式呈现,简洁清晰(含表情符号,<300字):1.基础概念与环境TensorFlow架构(计算图、会话->EagerE...
- 交叉验证和超参数调整:如何优化你的机器学习模型
-
准确预测Fitbit的睡眠得分在本文的前两部分中,我获取了Fitbit的睡眠数据并对其进行预处理,将这些数据分为训练集、验证集和测试集,除此之外,我还训练了三种不同的机器学习模型并比较了它们的性能。在...
- 机器学习交叉验证全指南:原理、类型与实战技巧
-
机器学习模型常常需要大量数据,但它们如何与实时新数据协同工作也同样关键。交叉验证是一种通过将数据集分成若干部分、在部分数据上训练模型、在其余数据上测试模型的方法,用来检验模型的表现。这有助于发现过拟合...
- 深度学习中的类别激活热图可视化
-
作者:ValentinaAlto编译:ronghuaiyang导读使用Keras实现图像分类中的激活热图的可视化,帮助更有针对性...
- 超强,必会的机器学习评估指标
-
大侠幸会,在下全网同名[算法金]0基础转AI上岸,多个算法赛Top[日更万日,让更多人享受智能乐趣]构建机器学习模型的关键步骤是检查其性能,这是通过使用验证指标来完成的。选择正确的验证指...
- 机器学习入门教程-第六课:监督学习与非监督学习
-
1.回顾与引入上节课我们谈到了机器学习的一些实战技巧,比如如何处理数据、选择模型以及调整参数。今天,我们将更深入地探讨机器学习的两大类:监督学习和非监督学习。2.监督学习监督学习就像是有老师的教学...
- Python 模型部署不用愁!容器化实战,5 分钟搞定环境配置
-
你是不是也遇到过这种糟心事:花了好几天训练出的Python模型,在自己电脑上跑得顺顺当当,一放到服务器就各种报错。要么是Python版本不对,要么是依赖库冲突,折腾半天还是用不了。别再喊“我...
- 神经网络与传统统计方法的简单对比
-
传统的统计方法如...
- 自回归滞后模型进行多变量时间序列预测
-
下图显示了关于不同类型葡萄酒销量的月度多元时间序列。每种葡萄酒类型都是时间序列中的一个变量。假设要预测其中一个变量。比如,sparklingwine。如何建立一个模型来进行预测呢?一种常见的方...
- 苹果AI策略:慢哲学——科技行业的“长期主义”试金石
-
苹果AI策略的深度原创分析,结合技术伦理、商业逻辑与行业博弈,揭示其“慢哲学”背后的战略智慧:一、反常之举:AI狂潮中的“逆行者”当科技巨头深陷AI军备竞赛,苹果的克制显得格格不入:功能延期:App...
- 时间序列预测全攻略,6大模型代码实操
-
如果你对数据分析感兴趣,希望学习更多的方法论,希望听听经验分享,欢迎移步宝藏公众号...
你 发表评论:
欢迎- 一周热门
- 最近发表
- 标签列表
-
- idea eval reset (50)
- vue dispatch (70)
- update canceled (42)
- order by asc (53)
- spring gateway (67)
- 简单代码编程 贪吃蛇 (40)
- transforms.resize (33)
- redisson trylock (35)
- 卸载node (35)
- np.reshape (33)
- torch.arange (34)
- npm 源 (35)
- vue3 deep (35)
- win10 ssh (35)
- vue foreach (34)
- idea设置编码为utf8 (35)
- vue 数组添加元素 (34)
- std find (34)
- tablefield注解用途 (35)
- python str转json (34)
- java websocket客户端 (34)
- tensor.view (34)
- java jackson (34)
- vmware17pro最新密钥 (34)
- mysql单表最大数据量 (35)