跨域请求问题

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

演示跨域问题:

  • 编辑一个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>
        <script src="https://cdn.staticfile.org/jquery/3.7.0/jquery.min.js"></script>
    </head>
    <body>
        <button id="btnGET">GET</button>
        <button id="btnPOST">POST</button>
    
        <script>
            $(function () {
                // 测试GET接口
                $('#btnGET').on('click', () => {
                    $.ajax({
                        type: 'GET',
                        url: 'http://localhost/get',
                        data: {
                            name: 'Zhangsan',
                            age: 20
                        },
                        success: (res) => {
                            console.log(res);
                        }
                    })
                });
                // 测试POST接口
                $('#btnPOST').on('click', () => {
                    $.ajax({
                        type: 'POST',
                        url: 'http://localhost/post',
                        data: {
                            name: '人间失格',
                            author: '太宰治'
                        },
                        success: (res) => {
                            console.log(res);
                        }
                    })
                });
            })
        </script>
    </body>
    </html>
    
  • 创建Express Server:

    const express = require('express');
    const app = express()
    
    app.use(express.json())
    
    app.get('/get', (req, res) => {
        res.send({
            status: 0,
            msg: '请求成功!'
        })
    })
    
    app.post('/post', (req, res) => {
        res.send({
            status: 0,
            msg: '请求成功!',
            data: req.body
        })
    })
    
    app.listen(80, () => {
        console.log('Server running at http://127.0.0.1/');
    })
    

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

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

  1. 安装cors

    npm i cors
    
  2. 导入cors

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

    app.use(cors())
    

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

完整示例如下:

const express = require('express');
const app = express()

// 导入并注册 cors 中间件
const cors = require('cors');
app.use(cors())

app.use(express.json())

app.get('/get', (req, res) => {
    res.send({
        status: 0,
        msg: 'GET请求成功!',
        data: req.query
    })
})

app.post('/post', (req, res) => {
    res.send({
        status: 0,
        msg: 'POST请求成功!',
        data: req.body
    })
})

app.listen(80, () => {
    console.log('Server running at http://127.0.0.1/');
})

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

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

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

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

    res.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请求头:

    res.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 请求(多个请求之间使用,分隔):

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

    res.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请求:

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

      app.delete('/delete', (req, res) => {
          res.send({
              status: 0,
              msg: 'DELETE请求成功!',
              data: req.body
          })
      })
      

    在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. 获取客户端发送过来的回调函数的名字。

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

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

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

    res.send(scriptStr)
    

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

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

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

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