一文带你彻底弄懂前后端之间的跨域,所有方式均用代码实现!!

826 阅读5分钟

相信各位都是久经沙场的前端人了,大伙应该都或多或少的知道一些和跨域相关知识吧,不过为了防止有的小伙伴是第一次看到了解到跨域,我们先来做一个简单的科普吧。

为什么会跨域?

出于浏览器的同源策略限制。同源策略(Sameoriginpolicy)是一种约定,它是浏览器最核心也最基本的安全功能,如果缺少了同源策略,则浏览器的正常功能可能都会受到影响,浏览器很容易受到XSS、CSRF等攻击。

什么是跨域?

跨域是由于不同源引起的,同源是指"协议+域名+端口"三者相同,即便两个不同的域名指向同一个ip地址,也非同源。这个在浏览器中的表现是这样的:

image.png

在上图中我们通过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的时候,可以看到控制台打印了:

image.png

所以从这个我们就可以知道,不是服务器不能发送,而是浏览器它拒绝接收!

设置接口响应头

对于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已发送')
  }

image.png

又或者可以将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所返回的内容了:

image.png

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')
})

接下来我们来看看效果吧:

image.png

我们在8000端口的页面向3000端口的服务器去请求图片,直接就加载成功了,浏览器并没有对它进行阻止!

与img标签相同情况的还有link标签

总结

以上的举例方法都是依托于前端页面和后端接口api非同源的场景下。

对于前端页面之间的非同源的跨域访问,本文并未涉及。

最后要特别说明一下,跨域请求产生时,请求是发出去了,也是有响应的,仅仅是浏览器同源策略,认为不安全拦截了结果,不将数据传递我们使用罢了。