是什么

跨域是指浏览器的不执行其他网站脚本的,由于浏览器的同源策略造成,是对JavaScript的一种安全限制。是浏览器出于安全才去的一种策略,而不是JavaScript本身的问题

一个网址的划分如下

image-20210505180207813

同源指的是协议、域名、端口 都要保持一致,只要有一处不同就视为跨域,参数部分不受影响

我们在做一个简单的例子,新建一个html,使用axios来进行网络请求,使用vscode插件live server将这个html 文件跑在5500端口

<!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>
  <script src="https://unpkg.com/axios/dist/axios.min.js"></script>
</head>
<body>
  <div id="box"></div>
  <script>
    axios.defaults.baseURL = 'http://127.0.0.1:3000'
    axios.get('/user').then(res => {
      document.getElementById('box').innerText = JSON.stringify(res)
    })
  </script>
</body>
</html>

然后从使用express运行一个简易的服务端程序,让他运行在3000端口

const express = require('express')

const app = express()

app.use('/user', (req, res) => {
  if (req.method === 'GET') {
    console.log('get请求');
    res.send('get user')
  }
})

app.listen(3000, () => {
  console.log('listening on port 3000');
})

然后就会看到浏览器的控制台报了一个错

image-20210505220244633

意思就是跨域了,但是这个跨域请求服务器已经响应了,但是响应结果被浏览器给拦截了,这一点从服务器的日志就可以看出来

image-20210505220448755

服务器已经响应并打印日志了

为什么

知道了什么是跨域,那么为什么浏览器要有这么一个机制 呢?

出于安全考虑!!!

你可以试想一下这么个情景,如果没有跨域访问的限制,你访问了一个恶意的网站,他有下面一段代码,请求知乎的私信接口,由于请求发送时会自动鞋带cookie所以数据在这里也可以获取,然后他再将这些数据通过某种手段收集起来,这样你的隐私消息就被别人看去了

var request = new XMLHttpRequest();
request.onreadystatechange = function () {
    if (request.readyState === 4) {
        if (request.status === 200) {
            console.log(request.responseText);
            // 后面再写个请求发到自己的接口
        }  
    }
}
// 发送请求:
request.open('GET', 'https://www.zhihu.com/message');
request.send();

所以浏览器要加一个同源策略来限制ajax请求,因为只有在同源下的请求一定是网站本身发出的

怎么办

解决跨域的问题有很多JSONP、nginx代理、CORS(跨域资源共享)、Nginx代理……,时至今日,最常用的就是CORS和Nginx了

JSONP

在发送Ajax请求时,不论请求的资源是何种类型都通不过同源策略,但是!!!你会发现<script>、<img>、<iframe>等没有这种限制。然后,js是支持json的,那么把json数据放到js中就可以接收了。废话少说上代码

<!--本地请求-->
<script>
  var local = function(data) {
    console.log('请求到的数据是:', data);
  }
</script>
<script src="http://local:3000/data?cb=jsonp"></script>

浏览器会自动把请求来的数据转为JavaScript代码执行,我们拼接接收的回调函数名和数据,将结果返回至浏览器,浏览器就能执行方法,就拿到了数据

// 服务器(express)
app.use('/data', (req, res) => {
  res.send(req.query.cb + '("data from jsonp!")')
})

app.listen(3000, () => {
  console.log('listening on port 3000');
})

运行页面效果如下

image-20200907174021099

完事,这就是jsonp,jquery的ajax中也提供了jsonp的请求方式,缺陷只能发送GET请求

CORS

关于CORS其实比较复杂,我们一点点来看

我们依旧还是使用express作为应用服务器,然后我们来编写如下代码

const express = require('express')

const app = express()

app.use('/user', (req, res) => {
  if (req.method === 'POST') {
    console.log('post请求');
    res.send('post user')
  }
app.listen(3000, () => {
  console.log('listening on port 3000');
})

然后继续使用axios发送请求

axios.defaults.baseURL = 'http://127.0.0.1:3000'
axios.post('/user?test=test',  {
  // 'Content-Type': 'application/json',
}).then(res => {
  document.getElementById('box').innerText = JSON.stringify(res.data)
})

然后浏览器就报跨域了

image-20210506193924573

这一段有点熟悉啊,没错,开头也有这么一段……这次服务器依旧是处理了请求,响应被浏览器拦截,这次我们使用CORS的方法来处理跨域,然后按照惯例打开百度,搜索跨域,找到这么一段代码贴上

app.all('*', function (req, res, next) {
  res.header("Access-Control-Allow-Origin", "*");
  res.header("Access-Control-Allow-Headers", "X-Requested-With");
  res.header("Access-Control-Allow-Methods", "PUT,POST,GET,DELETE,OPTIONS");
  res.header("X-Powered-By", ' 3.2.1')
  res.header("Content-Type", "application/json;charset=utf-8");
  next();
})

image-20210506195410138

哎!!行了,然后你继续编写代码,又碰到了这样一个请求,需要在请求头中设置Content-Type

axios.post('/user?test=test', {
  foo: 'bar'
}, {
  headers: {
    'Content-Type': 'application/json'
  }
}).then(res => {
  document.getElementById('box').innerText = JSON.stringify(res.data)
})

然后页面就会没有任何反应,打卡开发者工具的网络选项,可以看见同一个请求发送了两遍

image-20210506211751412

这是为什么呢?因为这属于复杂请求,在发送请求前浏览器会发送一个不携带数据的预检请求,只有预检请求通过时才会发送真实的请求

可以将代码稍作变动,捕获一下options,然后将状态设置为200,并且设置允许的请求头、请求方式等,原因我们稍后再说

app.use('/user', (req, res) => {
  if (req.method === 'POST') {
    console.log('post请求');
    res.send(`post:${req.query.test}`)
  }else if (req.method === 'OPTIONS') {
    console.log('预检请求');
    res.writeHead(200, {
      "Access-Control-Allow-Origin": "http://127.0.0.1:5500",
      "Access-Control-Allow-Headers": "Content-Type",
      "Access-Control-Allow-Methods": "POST",
    })
    res.end()
  }
})

可以看到此时两个请求都已经完成了

image-20210506214509622

预检请求

HTTP请求分为简单请求和复杂请求两种,简单请求不会触发预检,而复杂请求需要进行预检,因为浏览器的跨域机制只是拦截了响应,请求已经发送到了服务器,很可能对服务器的数据产生影响,所以复杂请求在发送前回先通过预检请求询问服务器是否允许发送跨域请求,只有得到肯定答复,浏览器才会发出真正的请求

怎么区分简单请求和复杂请求呢,只需要记住以下的简单请求,其余的都是复杂请求了

  • GET
  • HEAD
  • POST请求中的Content-Type是以下三个之一时
    • text/plain
    • multipart/form-data
    • application/x-www-form-urlencoded

显然我们上面的请求将Content-Type设置为了json,所以触发了预检,

现在已经知道了预检的机制,我们就可以在允许跨域时将options请求全部放行即可

app.options('*', (req, res) => {
  res.writeHead(200, {
            // 可以指定origin,也可以设置*即全部放行
      'Access-Control-Allow-Origin': 'http://127.0.0.1:5500',
        // 允许携带的首部,根据情况设置
      "Access-Control-Allow-Headers": "Content-Type",
        // 允许的请求方法
      "Access-Control-Allow-Methods": "PUT,POST,GET,DELETE,OPTIONS",
        // 在设置的时间内无须重复预检(单位:秒)
        "Access-Control-Max-Age": "1800"
    })
    res.end()
})

服务器会依据设置的允许放行的条件来处理预检请求,Access-Control-Allow-Origin是必须字段,其他事项可选字段只有符合origin的要求才会进行其他条件的判断,如果需要cookie则需要设置Access-Control-Allow-Credentials为true

注意这些限制只在浏览器中生效,在curl中无效


前端小白