Author:@德玛玩前端
Date: 2023-07-21
跨域
-
同源策略(Same Orgin Policy) 是浏览器的一个安全功能,不同源(协议名/域名/端口任一项不同)的客户端JS脚本在没有明确授权的情况下,不能读取对方的资源–称为禁止JS的"跨域请求"。所以a.com下的JS脚本采用AJAX读取b.com里面的文件数据是会报错的。
-
【注意】跨域问题证针对的是JS和AJAX,HTML本身没有跨域问题,比如a标签,是script标签,link标签,iframe标签,甚至form标签(可以直接跨域发送数据并接收数据)等
1、代理请求方案
-
客户端浏览器禁止跨域请求,而服务器请求另一个服务器上的资源是没有限制的,可以再当前服务器上为原本的跨域请求设置一个代理请求。
-
任何服务器端技术都可以将可以将自己作为客户端向其他服务器发送http请求,例如在Nodejs中可以使用
以配置vue脚手架内部服务器代理为例
// _解决跨域 vue.config.js
const {defineConfig} =require('@vue/cli-service')
module.exports=defineConfig({
devServer:{
//配置代理服务器
proxy:{
//定义处理那种请求得开头
//以后只要是以/demo为开头的请求,都会被处理
'/demo':{
//往哪个服务器发送请求
target:'http://127.0.0.1:3000/',
pathRewrite:{
//^代表字符串开头,实际发送请求时把/demo替换成''
//因为/demo并不是请求的一部分,只是代理的标识
'^/demo':''
}
},
//如果有多个继续配置...
}
}
})
2、JSONP方案
- JSONP(JSON with Padding)是JSON的一种使用模式,可用于解决主流浏览器的跨域数据访问的问题。
- 由于同源策略,浏览器禁止js跨域,但是script标签是允许跨域请求的,利用此特性,客户端使用script代替XHR发起跨域请求,服务器在JSON响应数据前后填充一些额外的内容,就称为"填充式JSON" —JSONP
以node作为serve为例
前端页面
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title></title>
</head>
<body>
<input type="text" name="" id="search-input" placeholder="请输入关键字"/>
<ul id="suggest-list">
</ul>
<script type="text/javascript">
function jsonp({url,jsonp,data},callback){
return new Promsie((reslove,reject)=>{
//创建script标签
let script=document.createElement('script')
//先拼出方法名
let callbackName=`jQuery_${Date.now()}`
// jQuery1300000000
// 给window添加一个全局变量,指向上面拼的方法名
// window.jQuery1300000000= (response)=>{}
window[callbackName]=function(response){
//把服务器返回的数据据传递给resolve,让Promise成功,并把response传入成功的回调
resolve(response)
callback?.(null,response)
delete window[callbackName]
document.head.removeChild(script)
}
// 如果用户给的url里面没有?,说明原理啊没有查询参数,querystring以?开头,否则说明
// 原来已经有了查询参数,直需要在后面追加参数即可
let querystring=url.index('?')==-1?'?':'&';
for(let key in data){
//把每个key value拼接成查询参数
querystring+=`${key}=${data[key]}&`
}
// https://www.baidu.com/sugrec?prod=pc&wd=a&cb=jQuery1300000000
script.src = `${url}${queryString}${jsonp}=${callbackName}`;
document.head.appendChild(script);
})
}
let searchInput = document.getElementById('search-input');
let suggestList = document.getElementById('suggest-list');
searchInput.addEventListener('input',(event)=>{
//调用jsonp方法
jsonp({
//url:'https://www.baidu.com/sugrec',//调用的jsonp接口
url:'http://localhost:3000/sugrec',
//传递jsonp回调函数名的参数名
jsonp:'cb',//&cb=jQuery110206648557500369825_1687695022686
data:{prod:'pc',wd:event.target.value}//携带的其它数据
}).then(response=>{
const suggestions = response.g;
let html = '';
for(let i=0;i<suggestions.length;i++){
html+=`<li>${suggestions[i].q}</li>`;
}
suggestList.innerHTML = html;
});
/* jsonp({
url:'https://www.baidu.com/sugrec',//调用的jsonp接口
//传递jsonp回调函数名的参数名
jsonp:'cb',//&cb=jQuery110206648557500369825_1687695022686
data:{prod:'pc',wd:event.target.value}//携带的其它数据
},response=>{
const suggestions = response.g;
let html = '';
for(let i=0;i<suggestions.length;i++){
html+=`<li>${suggestions[i].q}</li>`;
}
suggestList.innerHTML = html;
}); */
});
</script>
</body>
</html>
serve
const express=require('express')
const app=express()
app.get('/sugrec',(req,res)=>{
//https://www.baidu.com/sugrec?prod=pc&wd=a&cb=jQuery1300000000
//req.query={prod:'pc',cb:'jQuery1300000000',wd:'a'}
//解构出 cb,wd
const {cb,wd} = req.query;
const result={
g:Array.from({length:10},(_,i)=>({q:`${wd}${i+1}`}))
}
//result={g:[{q:'a1'},{q:'a2'}]}
res.send(`${cb}(${JSON.stringify(result)})`)
})
app.listen(8080,()=>{
console.log('server is started on port '+port)
})
3、CORS方案
-
跨域资源共享:Cross Origin Resource Sharing,是W3C标准,允许浏览器向跨域服务器,发出了XHR请求,从而克服了AJAX只能同源使用的限制。
-
基本上目前所有的浏览器都实现了CORS标准,其实目前几乎所有的浏览器ajax请求都是基于cors机制的。
-
对于客户端发起的简单请求,跨域的服务器需要在响应消息头部中添加 允许不同源访问控制头
以node作为serve为例
前端页面
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<div id="count"></div>
<script>
async function getCount(){
const response=await fetch('http://localhost:4000/count',{
credentials:"include" //如果让客户端在跨域的时候携带cookie,需要设置此项参数
})
const bodyData=await response.json()
document.getElementById('count').innerHTML=bodyData.count
}
getCount()
</script>
</body>
</html>
serve
const express=requrie('express')
const cookieParser=require('cookie-parser')
const app=express()
app.use(express.json()) //解析json请求体
app.use(express.urlencoded({extend:true})) //解析x-www-form-urlencoded
app.use(cookieParser())
app.use(express.static('public'))
const whiteList=[
"http://localhost:3000"
]
app.use((req,res,next)=>{
// 设置允许哪些来源跨域访问
// * 代表允许任何来源
// if(whiteList.includes(req.headers.orgin)){
// res.header('Access-Control-Allow-Origin',req.header.orgin)
// }
// The value of the 'Access-Control-Allow-Origin' header in the response must not be the wildcard '*' when the request's credentials mode is 'include'.
// 如果客户在请求的时候如果配置了credentials:'include',那么Access-Control-Allow-Origin就不能设置为*
res.header('Access-Control-Allow-Origin','http://localhost:3000')
// 如果客户端携带了cookie,服务器在预检的时候需要返回这样一个响应头
res.header('Access-Control-Allow-Credentials','true')
// 允许客户端跨域传递的请求头
res.header('Access-Control-Allow-Headers','Content-Type')
// 指定在跨域的时候向客户端暴露的响应头
res.header('Access-Control-Expose-Headers','custom-response-header')
// 正常情况下如何客户端向跨域向服务器发请求的时候,会先发一个预检请求,判断一下是否允许跨域
// 设置预检请求的结果可以缓存多少 3600秒就是一小时内不再发送预检请求
res.header('custom-response-header','custom')
if(req.method==='OPTIONS'){
// 如果客户端发过来的是一个预检用的OPTIONS请求,那么可以直接返回,因为这种请求只需要有响应头就行了,不需要响应头
return res.sendStatus(200)
}
next()
})
const users=[{id:1,name:'张三'}]
app.get('/users',(req,res)=>{
// origin: 'http://localhost:3000', 代表请求的来源
console.log(req.headers);
res.json(users)
})
app.post('/users',(req,res)=>{
// 获取 请求体
const newUser=req.body
// 获取users里最后一项的id
newUser.id=users[users.length-1].id+1
users.push(newUser)
res.json(newUser)
})
app.get('/count',(req,res)=>{
// 当客户端第一次访问服务端的时候肯定是没有cookie
let count=req.cookies.count||0
count++//1
// 然后服务器要向客户端种植cookie
res.cookie('count',count)
res.json({count})
})
const port = 4000;
app.listen(port,()=>console.log('server is started on port '+port));
4、possMessage 方案
a.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<iframe id="b-iframe" src="http://localhost:4000/b.html" onload="loaded()"></iframe>
<script>
window.addEventListener('message',(event)=>{
if(event.origin==="http://localhost:4000"){
console.log(event.data)
}
})
function loaded(){
// 获取b-iframe对应的window对象,也就是窗口对象
const bWindow = document.getElementById('b-iframe').contentWindow;
// 向bWindow发送一条跨域的消息
bWindow.postMessage('hello','http://localhost:4000');
}
</script>
</body>
</html>
b.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<script>
//监听别的窗口向我发过来的消息
//window.onmessage = ()=>{}
window.addEventListener('message',(event)=>{
//console.log(event.source);//来源窗口,就是说是哪个窗口向我发的消息
//console.log(event.origin);//来源域 http://localhost:3000
if(event.origin === 'http://localhost:3000'){
console.log(event.data);
event.source.postMessage('world',event.origin);
}
});
</script>
</body>
</html>