关于预检请求

272 阅读4分钟

跨域资源共享-CORS

在本地电脑双击打开html文件,浏览器地址栏上的url通常是这样的:file:///D:/xxx/index.html
以 file:///文件路径 这种打开文件的方式,叫做本地文件协议,它是浏览器用来访问并打开位于用户计算机(本地磁盘)上的文件的协议。
通过 本地文件协议 打开的页面,其 同源策略 会将每个文件甚至每个目录都视为一个独立的、互不信任的“源”。这导致向任何 HTTP 地址发送请求都会被视为 跨源请求,而浏览器默认会禁止这种请求,阻止一个“源”的文档或脚本与另一个“源”的资源进行交互,除非对方明确允许。
总而言之,服务器端几乎无法为 file: 协议来源的请求正确配置 CORS

当你尝试在 file: 协议页面中使用 fetch 或 XMLHttpRequest 发送 HTTP 请求时,浏览器控制台会抛出类似以下的错误:

  • Chrome/Edge:  Access to fetch at 'http://example.com/' from origin 'null' has been blocked by CORS policy: No 'Access-Control-Allow-Origin' header is present on the requested resource.
  • Firefox:  Cross-Origin Request Blocked: The Same Origin Policy disallows reading the remote resource at http://example.com/. (Reason: CORS header ‘Access-Control-Allow-Origin’ missing).

这个错误明确指出了请求是从源 'null'(即 file: 协议)发起的,并且被 CORS 策略阻止了。

所以,我们需要在本地起一个服务,去做跨域的测试。

做跨域测试前,需要了解以下的内容:

预检请求

什么情况下会触发预检请求?

预检请求的成功或者失败是服务端的配置决定的吗?分别是什么配置?

预检请求的作用,是浏览器问服务端,这个请求是否允许访问,如果服务端允许,则预检请求发送成功,然后浏览器才会正式发送第二次请求,对于复杂请求来说。如果预检请求失败了,说明服务端禁止这个请求

如果前端和后端都部署在同一台服务器上,也就是说前后端的地址的域名、端口完全一样,这个时候是否会发生预检请求? 预检请求的触发条件与请求的“源” 无关,而是与请求本身的特性有关。无论是否同源,只要你的请求满足以下任一条件,浏览器就会先发送预检请求:

  1. 使用了非简单方法 (Non-simple Methods):

    • 简单方法GETHEADPOST
    • 非简单方法PUTDELETECONNECTOPTIONSTRACEPATCH,以及任何自定义方法(如 UPDATE)。
  2. 设置了非简单首部 (Non-simple Headers):

    • 简单首部AcceptAccept-LanguageContent-LanguageContent-Type (值有限制,见下一条), DPRDownlinkSave-DataViewport-WidthWidth
    • 非简单首部:任何自定义首部(如 X-Requested-WithAuthorization(在某些情况下)),以及 Content-Type 的值不属于简单值。
  3. Content-Type 的值不是以下三种之一:

    -   `application/x-www-form-urlencoded`
    -   `multipart/form-data`
    -   `text/plain`

如果你使用 application/json 或 text/xml一定会触发预检

“跨域问题”本质上是浏览器和服务器之间的权限协商问题,而不是前端代码本身的问题。 解决跨域问题的关键永远在于后端服务器的正确配置
在正常情况下,简单请求绝对不会触发预检请求。而是会直接发送这个真正的请求(如 GETPOST)到服务器。

测试代码

const express = require('express');
const bodyParser = require('body-parser');
const app = express();
app.use(bodyParser.json());

app.all('*', (req, res, next) => {
    res.header('Access-Control-Allow-Origin', 'http://127.0.0.1:5500');
    res.header('Access-Control-Allow-Headers', 'Authorization,Content-Type');
    res.header('Access-Control-Allow-Methods', 'GET,POST,PUT');
    next();
})
app.put('/preflight-test', (req, res) => {
    res.json({
        msg: 'preflight-test'
    })
})

app.listen(8080, () => {
    console.log('Server is running on PORT 8080.');
})

复杂请求测试

以下是用vscode插件Live Server启动源为http://127.0.0.1:5500的本地服务器打开的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>
</head>

<body>
    <script>
        fetch('http://localhost:8080/preflight-test', {
            method: 'put'
        });
    </script>
</body>

</html>

image.png

image.png 可以看到,有两个一模一样的请求。但一个是OPTIONS请求,一个是PUT请求。

修改请求头

fetch('http://localhost:8080/preflight-test', {
    method: 'PUT',
    headers: {
        'X-Custom-Header': 'test-value'
    }
});

image.png 可以发现,虽然源匹配上了,但是还是发生了cors的报错。请求头不对也会导致跨域失败问题。 即使配置了允许源(Origin),但如果请求头(Headers)不匹配,同样会导致跨域预检请求失败。 Origin 只是跨域访问的“第一道关卡”。通过了它,只意味着浏览器愿意把你的请求“派送”到服务器。而请求的“具体内容”(方法、头信息)是否被接受,还需要由服务器返回的 Allow-Methods 和 Allow-Headers 来决定。

CORS失败 ≠ 连接失败。它特指浏览器在收到了服务器响应后,根据响应头中的CORS规则进行校验,发现权限不足,从而主动阻止前端JavaScript代码访问响应结果。

因此,OriginMethodsHeaders 三者是且(AND)  的关系,必须全部满足,请求才能成功。