持续创作,加速成长!这是我参与「掘金日新计划 · 10 月更文挑战」的第12天,点击查看活动详情
- Hello,这里是mouche,当然你也可以叫我某车,反正大家都爱这么叫😁
- 今天打算整理一下跨域相关内容
一、了解跨域
👉何为跨域
-
跨域并不是请求发不出去,请求能发出去,服务端能收到请求并正常返回结果,只是结果被浏览器拦截了
-
那为什么拦截呢,我们应该先了解一下什么是同源策略
-
MDN上的解释:同源策略是一个重要的安全策略,它用于限制一个origin的文档或者它加载的脚本如何能与另一个源的资源进行交互。它能帮助阻隔恶意文档,减少可能被攻击的媒介
-
具体的限制(三限制):
- 同源策略限制了来自不同源的
JavaScript
脚本对当前DOM
对象读和写的操作 - 同源策略限制了来自不同源的站点读取当前的
Cookie
、LocalStorage
等数据 - 同源策略限制了通过
XMLHttpRequest
等方式将站点的数据发送给不同源的站点
- 同源策略限制了来自不同源的
-
能使的标签(能跨域请求资源的标签):
<script src="..."></script>
<link rel="stylesheet" href="...">
- 通过
<img>
展示图片 - 通过
<video>
和<audio>
播放的多媒体资源 - 通过
<iframe>
载入的任何资源
-
那你限制了我这么多东西,我要正常使用肯定要跨域喽
👉何为同源
-
如果两个 URL 的协议(Protocol),域名(domain),端口(port)都相同的话,则这两个 URL 是同源
- 协议:定义了数据如何在计算机内和之间进行交换的规则的系统。例如:
http
,https
- 域名:可以通过DNS解析为IP地址 (这里即使两个不同的域名指向同一个IP地址,也是属于不同源的)
- 端口:http默认使用80端口,https默认使用443端口
- 协议:定义了数据如何在计算机内和之间进行交换的规则的系统。例如:
-
举个例子: 我们寻找跟掘金首页:
https://juejin.cn/
同源的URL | URL | 结果 | 原因 | | :------------------------------------------- | ------ | ------------------------------ | |http://juejin.cn/
| 不同源 | 协议不同 | |https://juejin.my.cn/
| 不同源 | 域名不同 | |https://juejin.cn:3030/
| 不同源 | 端口不一样(https默认443端口) | |https://juejin.cn/user/2375390426313406
| 同源 | 只有路径不一样 | |https://juejin.cn/post/7151609034699718692
| 同源 | 只有路径不一样 |
小结:跨域的出现是因为浏览器的同源策略对不同协议,域名,端口的URL在DOM对象的读写,Cookie等数据的获取,Ajax请求等方面有限制
二、跨域解决方案
JSONP(JSON with Padding)
- 最容易记住也最容易理解的应该就是JSONP了
- 我们前面提到了
<script src="..."></script>
是可以跨域请求资源的,那么JSONP就是利用了这一点了
🤜简单实现
- 先上一个简单的例子来理解思路
- 在客户端
<script>
function jsonp(){
return new Promise((resolve, reject) => {
//在全局定义一个doSomething的函数
window['doSomething'] = function(data) {
resolve(data);
}
})
}
jsonp().then(res=>{
//调用doSomething之后会在这里打印
console.log(res)
})
</script>
//使用script标签向后台/test路径发送参数callback为doSomething
<script src="http://localhost:3000/test?callback=doSomething"></script>
- 在服务端
app.get('/test',(req, res) => {
//通过res.send()方法-将处理好的内容,发送给客户端
let { callback } = req.query;
//返回一个函数的执行式doSomething(‘参数’)
res.send(`${callback}('这里是某车在使用简单的JSONP')`)
})
- 服务器返回
doSomething('这里是某车在使用简单的JSONP')
,因为我们已经声明过,所以客户端会直接调用doSomething
这个函数,然后打印出了:这里是某车在使用简单的JSONP
🤜流程图
- 全局声明一个回调函数
- 在
<script>
标签中,输入URL,向服务器传递该函数名和参数(可以通过问号传参:?callback=doSomething) - 服务器接收到请求后,需要进行处理:把传递进来的函数名和要返回的数据拼接为函数调用式,例如:传递进去的函数名是
doSomething
,要传回的数据是:这里是某车在使用简单的JSONP
,那么后台返回的即为doSomething('这里是某车在使用简单的JSONP')
- 客户端再调用执行之前声明的回调函数(
doSomething
),对返回的数据进行操作
🤜封装
但是在真正的开发中,我们不可能写一堆<script>
标签在那里,所以我们需要对jsonp进行封装
function jsonp({url, params, callback}){
return new Promise((resolve, reject) => {
//动态创建script标签
let script = document.createElement('script');
//处理传入的参数
params = {...params, callback};
//转换参数表达式
let arr = []
for(let key in params) {
arr.push(`${key}=${params[key]}`)
}
//在路径中,参数用 & 隔开
script.src = `${url}?${arr.join('&')}`
//添加 script 标签
document.body.appendChild(script);
//声明回调函数
window[callback] = function(data) {
//执行异步函数
resolve(data);
//请求完后移除该script标签
document.body.removeChild(script)
}
})
}
- 使用
jsonp({
url:'http://localhost:3000/test',
params:{ args:'这个是参数' },
callback: 'doSomething'
}).then(res=>{
console.log(res)
})
🤜优缺点
- 优点: 简单兼容性好,可用于解决主流浏览器的跨域数据访问的问题
- 缺点: 仅支持get方法具有局限性,不安全可能会遭受XSS攻击
CORS(Cross-Origin Resource Sharing)
- 实现CORS的关键在于后端:服务端设置
Access-Control-Allow-Origin
就可以开启 CORS。 该属性表示哪些域名可以访问资源,如果设置通配符则表示所有网站都可以访问资源 - 我们可以将请求分类为简单请求和非简单请求
🤲简单请求
判定:
-
请求方法是以下方三种方法之一
HEAD
GET
POST
-
请求头仅包含安全的字段,如以下几种字段
Accept
Accept-Language
Content-Language
Last-Event-ID
Content-Type
:只限于三个值application/x-www-form-urlencoded
、multipart/form-data
、text/plain
请求过程
- 客户端代码
btn.addEventListener('click',()=>{
const xhr = new XMLHttpRequest();
xhr.open('get', 'http://localhost:3000/test'); //向端口号为3000的发起请求
xhr.send();
xhr.onreadystatechange = function(){
if(xhr.readyState == 4) {
if(xhr.status >= 200 && xhr.status < 300) {
console.log(xhr.response) //如果请求成功,则打印后台返回的数据
}
}
}
})
- 服务端设置
app.get('/test',(req,res)=>{
//通过res.send()方法-将处理好的内容,发送给客户
res.send(`你好呀某车,你已经成功发起请求了`)
})
- 当你发送请求的时候(客户端为
5500端口
,服务端为3000端口
),什么也不设置就会报跨域错误
- 此时我们看一下当浏览器会给它的请求头加上
Origin
,值为它本身的协议+域名+端口
,这个时候服务器就能知道是谁在对他发送请求
- 那么服务器如果想要允许它访问的话,则在响应头加上
Access-Control-Allow-Origin
字段,值可以为*
表示允许所有源的访问,也可以为具体的源,我们这里给他设置为我们上述的Origin值
,同时也加上Access-Control-Allow-Methods
字段,设置可访问的请求方法
app.all("*",function(req,res,next){
//设置允许跨域的域名,*代表允许任意域名跨域
res.header("Access-Control-Allow-Origin","http://127.0.0.1:5500");
res.header("Access-Control-Allow-Methods",'PUT,POST,GET,DELETE,OPTIONS')
next();
})
- 此时你再点击发送请求就会发现:请求已经能够正常返回了
- 响应头显示的
Access-Control-Allow-Origin
即为我们可访问的源
🤲复杂请求
- 我们上述已经讲过什么是简单请求了,那么不符合上述条件的即为复杂请求
- 复杂请求发起请求的时候则是会在正式通信之前进行一次预检请求(
preflight request
) - 浏览器先询问服务器,当前源当前请求是否可以访问服务器资源,只有得到正确的答复,才会进行正式的请求
请求过程
- 我们依旧使用上面的代码,然后就请求方式改为
put
,此时发送的即为复杂请求,可以看到发送了两次请求,由于我们put
方法在上述已经被设置为可访问的方法,所以现在预检可以通过,我们也能正常请求到资源 - 如果我们将
put
方法从Access-Cntrol-Allow-Methods
移除,此时再进行请求,则发提示,在预检请求中发送PUT方法是不被允许的 - 此时
test
正式请求则报CORS错误
写到这里,我就想到了曾经看过的面试题:跨域请求如何携带cookie
🤲扩展
- 但是我想测试的时候受到了
SameSite
的限制,不允许第三方cookie
,本来想要关掉浏览器设置的,搜索了一下发现这个方法自从版本91之后就用不了,那么浅浅说一下方法就得了 - 只需要前端在发送Ajax请求的时候将
withCredentials
设置为true
const xhr = new XMLHttpRequest();
xhr.withCredentials = true;
- 后端设置
"Access-Control-Allow-Credentials
为true,同时注意Access-Control-Allow-Origin
值不为*
res.header("Access-Control-Allow-Credentials", "true");
🤲缺点
IE10以下不支持
小结:后端是关键,主要是设置
Access-Control-Allow-Origin
关于postMessage
和webSocket
的跨域方案我写在嗦嗦postMessage和webSocket这里了