跨域请求问题

跨域是指发送与当前服务的域名(或端口)不一致的请求。跨域问题的产生原因是浏览器不允许JS请求对域名不同或端口不同的服务发起请求(同源策略)。

演示跨域问题:

  • 编辑一个html页面:

     1<!DOCTYPE html>
     2<html lang="en">
     3<head>
     4    <meta charset="UTF-8">
     5    <meta name="viewport" content="width=device-width, initial-scale=1.0">
     6    <title>Document</title>
     7    <script src="https://cdn.staticfile.org/jquery/3.7.0/jquery.min.js"></script>
     8</head>
     9<body>
    10    <button id="btnGET">GET</button>
    11    <button id="btnPOST">POST</button>
    12
    13    <script>
    14        $(function () {
    15            // 测试GET接口
    16            $('#btnGET').on('click', () => {
    17                $.ajax({
    18                    type: 'GET',
    19                    url: 'http://localhost/get',
    20                    data: {
    21                        name: 'Zhangsan',
    22                        age: 20
    23                    },
    24                    success: (res) => {
    25                        console.log(res);
    26                    }
    27                })
    28            });
    29            // 测试POST接口
    30            $('#btnPOST').on('click', () => {
    31                $.ajax({
    32                    type: 'POST',
    33                    url: 'http://localhost/post',
    34                    data: {
    35                        name: '人间失格',
    36                        author: '太宰治'
    37                    },
    38                    success: (res) => {
    39                        console.log(res);
    40                    }
    41                })
    42            });
    43        })
    44    </script>
    45</body>
    46</html>
    
  • 创建Express Server:

     1const express = require('express');
     2const app = express()
     3
     4app.use(express.json())
     5
     6app.get('/get', (req, res) => {
     7    res.send({
     8        status: 0,
     9        msg: '请求成功!'
    10    })
    11})
    12
    13app.post('/post', (req, res) => {
    14    res.send({
    15        status: 0,
    16        msg: '请求成功!',
    17        data: req.body
    18    })
    19})
    20
    21app.listen(80, () => {
    22    console.log('Server running at http://127.0.0.1/');
    23})
    

只要页面打开的协议、地址或端口与服务器不同,浏览器就会阻止该请求的发送。解决方案有CORS和JSONP等,推荐使用CORS,因为JSONP仅支持GET请求。

在Express中,可以通过使用cors这个第三方中间件来解决跨域问题:

  1. 安装cors

    1npm i cors
    
  2. 导入cors

    1const cors = request('cors')
    
  3. 注册cors中间件:

    1app.use(cors())
    

    注:cors中间件要在所有路由之前注册。

完整示例如下:

 1const express = require('express');
 2const app = express()
 3
 4// 导入并注册 cors 中间件
 5const cors = require('cors');
 6app.use(cors())
 7
 8app.use(express.json())
 9
10app.get('/get', (req, res) => {
11    res.send({
12        status: 0,
13        msg: 'GET请求成功!',
14        data: req.query
15    })
16})
17
18app.post('/post', (req, res) => {
19    res.send({
20        status: 0,
21        msg: 'POST请求成功!',
22        data: req.body
23    })
24})
25
26app.listen(80, () => {
27    console.log('Server running at http://127.0.0.1/');
28})

CORS

CORS(Cross-Origin Resource Sharing,跨域资源共享)由一系列HTTP响应头组成,这些HTTP响应头决定浏览器是否阻止前端JS代码跨域获取资源。

浏览器的同源安全策路默认会阻止网页“跨域”获取资源。但如果接口服务器配置了CORS相关的HTTP响应头,就可以解除浏览器端的跨域访问限制。相当于服务器告诉浏览器我同意其它“域”来使用我的接口。

CORS的注意事项:

  • CORS主要在服务器端进行配置。客户端浏览器无须做任何额外的配置,即可请求开启了CORS的接口。
  • CORS在浏览器中有兼容性。只有支持XMLHttpRequest Level2的浏览器,才能正常访问开启了CORS的服务端接口(例如:IE10+、Chrome4+、FireFox3.5+)。

与CORS相关的响应头,基本上都是以Access-Control-Allow-开头。常见的CORS相关响应头有:

  • Access-Control-Allow-Origin

    1Access-Control-Allow-Origin: <origin> | *
    

    其中,origin参数的值指定了允许访问该资源的外域URL;如果要允许所有网站进行跨域请求,可以使用*符号。

    例如在服务端设置仅允许https://blog.linner.asia进行跨域请求:

    1res.setHeader('Access-Control-Allow-Origin', 'https://blog.linner.asia')
    
  • Access-Control-Allow-Headers

    默认情况下,CORS仅支持客户端向服务器发送如下的9种请求头:

    AcceptAccept-LanguageContent-LanguageDPRDownlinkSave-DataViewport-WidthWidthContent-Type(值仅限于text/plainmultipart/form-dataapplication/x-wwnw-form-urlencoded三者之)。

    如果客户端向服务器发送了额外的请求头信息,则需要在服务器端,通过Access-Control-Allow-Headers对额外的请求头进行声明;否则这次请求将会失败。

    例如在服务器中通过设置Access-Control-Allow-Headers来允许客户端发送Content-TypeXCustom-Header请求头:

    1res.setHeader(`Access-Control-Allow-Headers`, 'Content-Type, XCustom-Header')
    

    注:res.setHeader()中设置Access-Control-Allow-Headers时,多个请求头之间使用,分隔。

  • Access-Control-Allow-Methods

    默认情况下,CORS仅支持客户端发起GET、POST、HEAD请求。如果客户端希望通过PUT、DELETE等方式请求服务器的资源,则需要在服务器端。通过Access-Control-Allow-Methods来指明实际请求所允许使用的HTTP方法,

    • 只允许 POST、GET、DELETE、HEAD 请求(多个请求之间使用,分隔):

      1res.setHeader('Access-Control-Allow-Methods', 'POST, GET, DELETE, HEAD')
      
    • 使用*符号允许所有的 HTTP 请求:

    1res.setHeader('Access-Control-Allow-Methods', '*')
    

客户端在请求CORS接口时,根据请求方式和请求头的不同,可以将CORS的请求分为两大类,分别是:

  • 简单请求

    同时满足以下两大条件的请求,就属于简单请求:

    • 请求方式必须是 GET、POST、HEAD 之一。

    • HTTP头部信息不超过以下几种字段:

      无自定义头部字段、AcceptAccept-LanguageContent-LanguageDPRDownlinkSave-DataViewport-WidthWidthContent-Type(值仅限于text/plainmultipart/form-dataapplication/x-wwnw-form-urlencoded三者之)。

    特点:客户端与服务器之间只会发生一次请求。

  • 预检请求

    只要符合以下任一条件的请求,都需要进行预检请求:

    • 请求方式为 GET、POST、HEAD 之外的请求类型。
    • 请求头中包含了自定义头部字段。
    • 发送的请求数据包含application/json格式的数据。

    在浏览器与服务器正式通信之前,浏览器会先发送OPTION请求进行预检,以获知服务器是否允许该实际请求,所以这一次的OPTION请求被称为“预检请求”。服务器成功响应预检请求后,才会发送真正的请求,井且携带真实数据。

    特点:客户端与服务器之间会发生两次请求,OPTION预检请求成功之后,才会发起真正的请求。

    演示:

    • 在HTML中添加以下按钮和Ajax请求:

      1<button id="btnDELETE">DELETE</button>
      
       1// 测试DELETE接口(演示预检请求)
       2$('#btnDELETE').on('click', function () {
       3    $.ajax({
       4        type: 'DELETE',
       5        url: 'http://127.0.0.1/delete',
       6        data: {
       7            id: '1'
       8        },
       9        success: function (res) {
      10            console.log(res);
      11        }
      12    })
      13});
      
    • 在Express Server中添加以下接口:

      1app.delete('/delete', (req, res) => {
      2    res.send({
      3        status: 0,
      4        msg: 'DELETE请求成功!',
      5        data: req.body
      6    })
      7})
      

    在Edge、Firefox等能显示预检请求的浏览器(Google Chrome需要在chrome://flags/#out-of-blink-cors中开启显示预检请求)中打开HTML页面,点击DELETE按钮发起请求。然后在开发者工具的网络监测中可以看到发送了一条大小为0的OPTION预检请求。


JSONP 接口

浏览器端通过<script>标签的src属性,请求服务器上的数据。同时,服务器返回一个函数的调用。这种请求数据的方式叫做JSONP。

JSONP的特点:

  • JSONP不属于真正的Ajax请求,因为它没有使用XMLHttpRequest这个对象。
  • JSONP仅支持GET请求,不支持POST、PUT、DELETE等请求。

实现JSONP接口的步骤:

  1. 获取客户端发送过来的回调函数的名字。

    1const funcName = req.query.callback
    
  2. 得到要通过JSONP形式发送给客户端的响应数据。

  3. 根据前两步得到的数据,拼接出一个函数调用的字符串。

    1const data = /* get response data... */
    2// 拼接函数调用的字符串
    3const scriptStr = `${funcName}(${JSON.stringify(data)})`
    
  4. 把上一步拼接得到的字符串,响应给客户端的<script>标签进行解析执行。

    1res.send(scriptStr)
    

客户端进行JSONP请求的示例:

1<button id="btnJSONP">JSONP</button>
 1$('#btnJSONP').on('click', function () {
 2    $.ajax({
 3        type: 'GET',
 4        url: 'http://127.0.0.1/jsonp',
 5        dataType: 'jsonp',  // 表示发起JSONP请求
 6        success: function (res) {
 7            console.log(res);
 8        }
 9    })
10});

完整的JSONP接口实现示例如下:

 1app.use('/jsonp', (req, res) => {
 2    // TODO: 定义 JSONP 接口具体的实现过程
 3    // 获取回调函数名称
 4    const funcName = req.query.callback
 5    // 获取响应数据
 6    const data = {
 7        name: 'Zhangsan',
 8        age: 22
 9    }
10    // 拼接函数调用的字符串
11    const scriptStr = `${funcName}(${JSON.stringify(data)})`
12    // 将函数调用字符串响应给客户端的 <script> 标签进行解析执行
13    res.send(scriptStr)
14})
15// 如果同时启用了 CORS 和 JSONP,CORS中间件要放在JSONP接口后面,否则JSONP接口也会以CORS形式进行相应