我报名参加金石计划一期挑战——瓜分10万奖池,这是我的第2篇文章,点击查看活动详情
想必大家都知道,前端的数据一般都是来源于后端,后端对数据做好一系列处理之后,前端就可以对后端发起接口请求,请求数据。
可是,当我们发送接口请求的时候,浏览器会有这么一个策略 “同源策略” 将其拦截,导致前端无法拿到想要的数据,那么我们该怎么应对同源策略呢?
同源策略
那么什么是同源策略呢?
同源策略:即前端的协议号、域名、端口号与后端的协议号、域名、端口号都一致符合请求,浏览器才不会对其进行拦截,这就是同源策略。
什么是协议号?什么是域名?什么又是端口号?
这里咱们拿百度的链接来说:
这是一个百度的链接:https: // www.baidu.com :8080 /userInfo
https
就是协议号。
www.baidu.com
就是域名。
:8000
就是端口号(一般情况下端口号都是隐式的,所以我们看不见)。
而后面的/userInfo
等等都是路由路径啦
跨域拦截发生在什么时候呢?
前端向后端发送接口请求数据大致可以分为两个阶段,第一阶段是前端把请求发给后端,第二阶段是后端根据前端的请求将数据返回给前端。
那么问题来了,浏览器跨域拦截是发生在第一阶段前端发送接口请求给后端的时候,还是发生在第二阶段后端返还数据给前端呢?
这里咱们给大家做一个例子,大家就能很快理解啦。
咱们创建两个文件夹,分别为client,server
;
然后打开server文件夹的终端,输入以下命令行(前提是你有安装配置好npm哈):
npm init -y //初始化文件夹
npm i koa //安装koa
那么,(如果你下了node.js
)之后咱们就可以在server
里面引入koa
然后运行后端啦;
咱们在server文件夹里创建一个app.js
文件,然后写下以下代码:
const Koa = require('koa')
const app = new Koa();
const main = (ctx,next) =>{
console.log(ctx.query.name)
ctx.body = 'hello world'
}
app.use(main)
app.listen(3000,()=>{
console.log('项目已启动!')
})
然后在server文件夹下终端输入node app.js
命令行运行后端代码,运行成功就有如下效果:
然后去浏览器的url输入localhost:3000
回车,那么咱们就能看到后端的运行啦。
后端这边弄完了,接下来咱们就该去前端,对后端发送接口请求了,打开client
文件夹,然后创建一个index.html文件,引入jquery
的js;然后用$.ajax
发起接口请求,于是有了以下代码:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<script src="https://cdn.bootcdn.net/ajax/libs/jquery/3.6.1/jquery.js"></script> //引入jquery
<title>Document</title>
</head>
<body>
<button id="btn">获取数据</button> //点击发起接口请求
<script>
let btn = document.getElementById('btn')
btn.addEventListener('click',()=>{
$.ajax({ //jquery接口请求
url:'http://localhost:3000',
data:{name:'江湖'}, //传给后端的数据
method:'get',
success(res){
console.log(res) //如果接口请求成功,那么回调会打印res
}
})
})
</script>
</body>
</html>
然后咱们去运行这个前端,点击按钮,发起接口:
然后我们打开页面检查,可以发现报错了:
记住这个报错,这就是跨域被拦截然后才会报的错,然后咱们再回到后端的终端这边:
可以看到后端这里咱们打印出来了前端传给后端的数据,说明前端发给后端的请求已经被后端接收到了,可是前端仍然没有拿到数据,报错也显示被跨域拦截了,说明后端返还数据给前端的时候,数据被跨域拦截下来了。
那么咱们可以得出结论:跨域拦截发生在后端拿到前端接口请求后返还数据给前端的时候。
怎么解决跨域拦截呢?
解决跨域拦截的方法有很多,但是大多方法都是有一定的弊端的,这里咱们主要讲一下三种比较基础的方法;
这三种方法分别是:
1. JSONP
2. Cors(cross-Origin Resouce Sharing)(后端开启)
3. node代理 (vue.config.js)
JSONP
什么是JSONP
呢?
讲到JSONP
那么咱们先讲到html
里的一些标签吧,不知道大家有没有想起来html
里面有几个标签也是可以向其它地方请求数据,而且还不会被跨域拦截奥。
它们就是:
link标签:link
标签有一个src
属性,link
标签可以直接向src
属性里的链接拉取数据,可是它却不会被跨域拦截。
img标签:同上,具有src
属性。
script标签:同上,具有src
属性。
而JSONP
方案就是利用这些标签属性来进行请求数据,从而做到不被跨域拦截。
那么咱们就接上之前的用例啦,咱们将client
文件夹下的index.html
里面script
标签里面的代码替换成如下(其它部分一切不变):
<script>
const josnp = (url,params,cb) =>{
return new Promise((resolve,reject)=>{
const script = document.createElement('script');
params = {...params,cb:cb}
const arr = Object.keys(params).map(key => `${key}=${params[key]}`)
script.src = `${url}?${arr.join('&')}`
document.body.appendChild(script)
//后端会返回一个函数给前端
window[cb] = (data)=>{
resolve(data)
}
})
}
let btn = document.getElementById('btn')
btn.addEventListener('click',()=>{
josnp('http://localhost:3000',{name:'江湖',age:18},'callback').then(res=>{
console.log(res)
})
})
</script>
定义一个josnp
函数,在下面的btn.addEventListener
的点击事件里面进行监听,一旦点击button
按钮,则调用josnp
函数,并向其中传参,第一个参数'http://localhost:3000'
是我们要去的后端地址,
第二个参数{name:'江湖',age:18}
是前端向后端传的数据;
第三个参数'callback'
是一个字符串,让后端将数据放入这个字符串后面,让它传回前端后成为一个函数调用指令。
再回到josnp函数里面,
const script = document.createElement('script');
是创建一个script标签;
params = {...params,cb:cb}
是将我们要传给后端的数据解构放入一个对象里面,同时把我们要从后端拿到的数据也放入里面;
const arr = Object.keys(params).map(key =>
{params[key]})
则是将这个对象里面的数据转化成一个'key=value'字符串的数组的形式
script.src = ${url}?${arr.join('&')}
则是将我们要向后端发的链接地址放入src
属性中,发起请求。
document.body.appendChild(script)
是将srcipt标签放入html标签中,让它生效(img标签比较特殊,不需要放入html标签里也是可以生效的)。
window[cb] = (data)=>{ resolve(data) }
这是重点,后端传回来的数据应该是这个callback('后端给前端的数据'),是一个调用指令,那么就会调用一个名字叫callback
的函数,那么这个函数在哪呢?
它就是window[cb]
了,cb是我们传参的callback,是在window下生效的,可以被返还过来的指令直接调用到的。
前端代码解读就这样的,那么前端是要求后端返还给咱们一个指令,咱们就可以去后端做一些操作啦。
server
文件夹下app.js
代码修改如下:
const Koa = require('koa')
const app = new Koa();
const main = (ctx,next) =>{
console.log(ctx.query)
const {name,age,cb} = ctx.query
const userInfo = `${name}今年${age}岁`
const str = `${cb}(${JSON.stringify(userInfo)})`
ctx.body = str
}
app.use(main)
app.listen(3000,()=>{
console.log('项目已启动!')
})
这里无非是对前端给我们的数据做了一些处理,然后返还给前端一个callback(userInfo)
的调用指令;
那么咱们再次运行前后端,结果是这样的:
前端打印:
后端打印:
可以看到的是,前端拿到了后端传过来的数据,后端也拿到了前端给后端传的数据,跨域拦截被我们避开了。
这就是
JSONP
方法避开跨域拦截,利用html
特殊属性的标签来达到避开跨域拦截,但是需要注意的是,利用src就注定发送的请求类型是get请求,那么我们传给后端的数据将会被加在链接后面的路径显示出来,从而达不到数据保密的效果,这就是它的弊端。
Cors(cross-Origin Resouce Sharing)(后端开启)
Cors方案咱们来讲两种写法,一种比较简单,只需要引入一些东西即可,一种则是咱们用原生JS去实现同样的效果。
而Cors方法避开跨域拦截的原因则是让后端对前端的协议号、域名、端口号放开,放入白名单,让前端请求数据不再会被跨域拦截,当然,这个方法的弊端是只适用于开发环境,一旦发布上线了,放开的白名单就会导致他人可以直接请求服务器数据,这就是它的弊端。
Cors方法一
首先我们在后端已安装koa文件的基础上,再调用指令行npm install @koa/cors --save
安装一个cors
,于是我们就可以在后端加上这两行代码:
const cors = require('@koa/cors');
app.use(cors()); //后端开启cors,允许跨域操作
那么后端代码就变成了这样:
const Koa = require('koa')
const app = new Koa();
const cors = require('@koa/cors');
app.use(cors()); //后端开启cors,允许跨域操作
const main = (ctx,next) =>{
console.log(ctx.query.name)
ctx.body = 'hello world'
}
app.use(main)
app.listen(3000,()=>{
console.log('项目已启动!')
})
前端代码不作任何改变,依旧如下:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<script src="https://cdn.bootcdn.net/ajax/libs/jquery/3.6.1/jquery.js"></script>
<title>Document</title>
</head>
<body>
<button id="btn">获取数据</button>
<script>
let btn = document.getElementById('btn')
btn.addEventListener('click',()=>{
$.ajax({
url:'http://localhost:3000',
data:{name:'江湖'},
method:'get',
success(res){
console.log(res)
}
})
})
</script>
</body>
</html>
只需要这样引入一个cors
就可以让后端放开对前端请求的白名单了,那么前端就可以发送请求到后端并拿到数据啦。
是不是特别简单,只需要作为前端的你能够压迫后端安装一个cros,以及让他多加两行代码,haha。
cros方法二
cros第二个方法跟第一个cros方法是一样的,只不过第一个方法咱们采用的是直接外部安装引入的,而第二个方法则是咱们自己去手写这部分封装的代码,从而达到与第一个cros相同的效果。
那么我们将后端代码全部注释或者删除,然后重新启动web请求,前端代码仍旧不变,后端代码如下:
const http = require('http')
const server = http.createServer((req,res)=>{ //调用http.createServer启动服务
//开启cors,后端在响应头中设置
res.writeHead(200,{
"Access-Control-Allow-Origin":"*", //协议白名单,*为所有都可以访问
"Access-Control-Allow-Methods":"GET,POST,PUT,OPTIONS",
"Access-Control-Allow-Headers":"Content-Type" //允许前端设定请求头的类型,不管向浏览器返回什么类型都可以
})
res.end('hello cors') //向前端输出返回的东西
})
server.listen(3000,()=>{ //监听电脑的一个端口
console.log('项目已启动!')
})
那么,我们这里的后端的代码主要是在响应头中设置了一些操作,那么什么是响应头呢?
来,我们随便打开一个网页进行检查,这里咱们打开一个百度,然后去到Network(网络)里检查,你会发现,一个请求会有三部分,请求头,请求体,响应头。
而我们之前也说过了,前端发请求给后端,从前端到后端这个过程中,我们可以理解为请求头在发挥作用,我们对于向后端的数据要求都可以在请求头中操作好。
请求体咱们就暂且不说,而响应头呢,则是后端接收到前端的请求后,返还给前端相应的数据,而在返还前端的过程中做操作的地方啦,这里我们就是在响应头中操作让后端放开白名单,然后使浏览器不再会跨域拦截后端返还给前端的数据。
用这种操作我们仍旧能在前端拿到请求的数据。
注意:这种操作只适用于开发环境下。
node代理 (vue.config.js)
node代理其实是一种在利用了跨域拦截的特性来解决跨域的一种方法。
我们都知道,跨域是发生在后端向前端返还数据的时候,那么,拦截发生在哪里?发生在浏览器,这是浏览器对于后端返还给前端的数据拦截,所以是发生在浏览器里。
那么,简单来说,是不是只要我们的数据不经过浏览器,那么是不是就可以不被拦截呢?
事实确实如此,但是怎么做到请求数据不经过浏览器呢?
这里咱们就采用一个投机取巧的方式,在自己的服务器写一个后端向别人的后端请求数据,这个样子是不会经过浏览器的,因为只有后端返还数据给前端才会经过浏览器,而一个后端向另外一个后端请求数据,再由另外一个后端返还数据回来给这个后端,就不会经过浏览器了。
图解就是这样的:
代码操作如下:
别人的后端:
//别人的后端
const Koa = require('koa')
const app = new Koa();
const main = (ctx,next) =>{
console.log(ctx.query.name)
ctx.body = 'hello world'
}
app.use(main)
app.listen(3000,()=>{
console.log('项目已启动!')
})
咱们的后端:
//自己的后端
const http = require('http')
const server = http.createServer((req,res)=>{
//开启cors,后端在响应头中设置
res.writeHead(200,{
"Access-Control-Allow-Origin":"*", //协议白名单,*为所有都可以访问
"Access-Control-Allow-Methods":"GET,POST,PUT,OPTIONS",
"Access-Control-Allow-Headers":"Content-Type" //允许前端设定请求头的类型,不管向浏览器返回什么类型都可以
})
//向别人的后端请求数据
http.request({
host:'127.0.0.1',
port:'3000',
path:'/',
method:'GET'
},proxyRes=>{
// console.log(proxyRes)
proxyRes.on('data',result=>{
// console.log(result.toString()) //result是一个buffer流字符,
res.end(result.toString()) //toString()将buffer流字符转化成正常字符,返还给前端
})
}).end()
})
server.listen(3001,()=>{
console.log('my项目已启动!')
})
咱们的前端:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<script src="https://cdn.bootcdn.net/ajax/libs/jquery/3.6.1/jquery.js"></script>
<title>Document</title>
</head>
<body>
<button id="btn">获取数据</button>
<script>
let btn = document.getElementById('btn')
btn.addEventListener('click',()=>{
$.ajax({
url:'http://localhost:3001',
data:{name:'江湖'},
//为了告诉后端,你返回的响应头类型应该是xxx
headers:{Accept:"application/json;charset=utf-8"},
method:'get',
success(res){
console.log(res)
}
})
})
</script>
</body>
</html>
这种node代理的方法就是自己造一个后端作为数据的中转站,然后在自己的后端进行取消对前端的跨域拦截,设置白名单。(同样只是适用于开发环境)