相信各位都是久经沙场的前端人了,大伙应该都或多或少的知道一些和跨域相关知识吧,不过为了防止有的小伙伴是第一次看到了解到跨域,我们先来做一个简单的科普吧。
为什么会跨域?
出于浏览器的同源策略限制。同源策略(Sameoriginpolicy)是一种约定,它是浏览器最核心也最基本的安全功能,如果缺少了同源策略,则浏览器的正常功能可能都会受到影响,浏览器很容易受到XSS、CSRF等攻击。
什么是跨域?
跨域是由于不同源引起的,同源是指"协议+域名+端口"三者相同,即便两个不同的域名指向同一个ip地址,也非同源。这个在浏览器中的表现是这样的:
在上图中我们通过koa起了两个服务,一个8000端口的渲染html页面的服务,一个3000端口的后端api服务,然后我们在html页面里去fetch请求3000端口的api就发生了跨域,下面是代码,感兴趣的可以自己去启一个试试:
// cors.html
<!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">
<title>Document</title>
</head>
<body>
<p>我想跨域啊</p>
<script>
fetch('http://localhost:3000/cors').then(res => res.json()).then(res => {
// 将接口返回值渲染到页面上
document.querySelector('p').innerHTML = res.msg
}, err => {
console.log(err)
})
</script>
</body>
</html>
// corshtml.js
const Koa = require('Koa')
const app = new Koa()
const { readFileSync } = require('fs')
app.use((ctx, next) => {
ctx.set("Content-Type", "text/html");
ctx.body = readFileSync('./cors.html')
})
app.listen(8000, () => {
console.log('cors html server')
})
// api.js
const Koa = require('koa')
const app = new Koa()
app.use((ctx, next) => {
if (ctx.url === '/cors') {
ctx.body = {
msg: '跨域成功O.o',
code: 200
}
}
})
app.listen(3000, () => {
console.log('cors api server')
})
如何解决跨域问题
在想着如何解决跨域问题之前,我们需要先知道一个很重要的知识点:跨域到底是浏览器不能接收还是
服务器不能发送呢?我们可以来试试,我们在api.js里添加一段代码:
ctx.body = {
msg: '跨域成功O.o',
code: 200
}
console.log('跨域api已发送')
我们在body之后去添加了一段打印,这个时候当我们去刷新页面重新访问后端的api的时候,可以看到控制台打印了:
所以从这个我们就可以知道,不是服务器不能发送,而是浏览器它拒绝接收!。
设置接口响应头
对于xhr/fetch的跨域问题的解决办法其实有好几种,其中的首当其冲的一种就是设置接口的响应头,秉承着能轻松自己就别为难自己的原则,我们可以让后端去设置api接口的响应头的Access-Control-Allow-Origin的值为http://localhost:8000,这样设置就代表当前api接口允许这个页面进行跨域访问,这个时候浏览器就不会对它进行跨域拦截:
// api.js
if (ctx.url === '/cors') {
ctx.set('Access-Control-Allow-Origin', 'http://localhost:8000')
ctx.body = {
msg: '跨域成功O.o',
code: 200
}
console.log('跨域api已发送')
}
又或者可以将Access-Control-Allow-Origin的值设置为*这样就代表着任何非同源的页面发起的请求都可以拿到返回的内容。
代理服务请求转发
从上面我们可以知道,对于浏览器的跨域是因为浏览器的安全策略,而服务器之间不存在跨域的,那么我们就可以让文件服务器去转发我们的api请求,让文件服务器去请求api接口服务器,然后再把对应的数据返回给前端。首先我们改一下html文件,因为是直接要做请求转发,所以我们html里的fetch是去请求当前域名:
// cors.html
<!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">
<title>Document</title>
</head>
<body>
<p>我想跨域啊</p>
<script>
fetch('http://localhost:8000/cors').then(res => res.json()).then(res => {
// 将接口返回值渲染到页面上
document.querySelector('p').innerHTML = res.msg
}, err => {
console.log(err)
})
</script>
</body>
</html>
然后我们还需要去修改文件服务器的内容,让它实现请求转发:
// corshtml.js
const Koa = require('Koa')
const app = new Koa()
const { readFileSync } = require('fs')
const http = require('http')
app.use(async (ctx, next) => {
if (ctx.path === '/cors') {
const res = await proxyApi(ctx.path)
ctx.body = res
} else {
ctx.set("Content-Type", "text/html");
ctx.body = readFileSync('./cors.html')
}
})
const proxyApi = path => {
return new Promise((resolve, reject) => {
var data = ''
const req = http.get(`http://localhost:3000` + path, (res) => {
res.on('data', chunk => {
data += chunk
})
res.on('end', () => {
resolve(data)
})
})
})
}
app.listen(8000, () => {
console.log('cors html server')
})
然后我们刷新页面就可以拿到后端api所返回的内容了:
JSONP解决跨域
对于JSONP而言,浏览器也是不会对它进行一个跨域拦截的,那么JSONP是怎样去请求的呢?其实JSONP是通过一个script标签的src属性去请求对应的一个接口url,然后将我们的回调函数拼接在url后面,后端接口也需要拿到我们的回调函数去拼接参数进行返回。我们先来改写html代码:
// cors.html
<!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">
<title>Document</title>
</head>
<body>
<p>我想跨域啊</p>
</body>
<script>
function callbackFunc(res) {
document.querySelector('p').innerHTML = res.msg
}
</script>
<script src="http://localhost:3000/cors?callback=callbackFunc"></script>
</html>
从上面的代码中我们可以看到,我们定义了一个callbackFunc的回调方法,该回调方法接收一个参数,然后我们另起一个script标签去请求后端api并且把回调方法给拼接上。
接下来我们改写后端的api接口,我们需要对传参的回调函数进行处理:
// api.js
const Koa = require('koa')
const app = new Koa()
app.use((ctx, next) => {
console.log(ctx.query)
if (ctx.path === '/cors') {
// ctx.set('Access-Control-Allow-Origin', 'http://localhost:8000')
const resContent = {
msg: '跨域成功O.o',
code: 200
}
const res = `${ctx.query.callback}(${JSON.stringify(resContent)})`
ctx.body = res
console.log('跨域api已发送')
}
})
app.listen(3000, () => {
console.log('cors api server')
})
ImgSrc
当我们通过img标签去加载非同源资源的时候,浏览器是允许我们跨域的,我们来做一个简单的尝试,我们现在html文件中创建一个img标签,然后用img标签src引用非同源的图片资源:
// cors.html
<!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">
<title>Document</title>
</head>
<body>
<p>我想跨域啊</p>
<img src="http://localhost:3000/img" />
<!-- <script>
fetch('http://localhost:8000/cors').then(res => res.json()).then(res => {
// 将接口返回值渲染到页面上
document.querySelector('p').innerHTML = res.msg
}, err => {
console.log(err)
})
</script> -->
</body>
<script>
function callbackFunc(res) {
document.querySelector('p').innerHTML = res.msg
}
</script>
<script src="http://localhost:3000/cors?callback=callbackFunc"></script>
</html>
接着我们就要api服务器去响应对应的/img请求:
// api.js
const Koa = require('koa')
const app = new Koa()
const { readFileSync } = require('fs')
app.use((ctx, next) => {
console.log(ctx.query)
if (ctx.path === '/cors') {
// ctx.set('Access-Control-Allow-Origin', 'http://localhost:8000')
const resContent = {
msg: '跨域成功O.o',
code: 200
}
const res = `${ctx.query.callback}(${JSON.stringify(resContent)})`
ctx.body = res
console.log('跨域api已发送')
} else if (ctx.path === '/img') {
ctx.body = readFileSync('./handsome.jpg')
}
})
app.listen(3000, () => {
console.log('cors api server')
})
接下来我们来看看效果吧:
我们在8000端口的页面向3000端口的服务器去请求图片,直接就加载成功了,浏览器并没有对它进行阻止!
与img标签相同情况的还有link标签
总结
以上的举例方法都是依托于前端页面和后端接口api非同源的场景下。
对于前端页面之间的非同源的跨域访问,本文并未涉及。
最后要特别说明一下,跨域请求产生时,请求是发出去了,也是有响应的,仅仅是浏览器同源策略,认为不安全拦截了结果,不将数据传递我们使用罢了。