什么情况下需要发送 HTTP 方法为 OPTIONS 的 preflight 请求

40 篇文章 1 订阅

在做项目时,很多时候发送一个post请求,是先发送一个option请求,然后再发送post请求,一直这么用之前也没有仔细思考,今天有时间,好好了解一下为什么会多一次请求。

疑问1:什么是options请求

OPTIONS请求方法的主要用途有两个:

1、获取服务器支持的HTTP请求方法;
2、用来检查服务器的性能。例如:AJAX进行跨域请求时的预检,需要向另外一个域名
   的资源发送一个HTTP OPTIONS请求头,用以判断实际发送的请求是否安全。

这是浏览器给我们加上的,后端并没有做任何操作。

疑问2:为什么会用到options请求

这得从浏览器同源策略和跨域说起,具体可阅读也谈谈同源策略和跨域问题浏览器同源政策及其规避方法,这里不在赘述。

解决跨域问题的方法有很多种,CORS是比较好的解决方案,我们的项目也是用的这种模式,这个模式会有”预检”的请求,也就是正常请求之前的options请求。

关键词:CORS 跨域资源共享

CORS是一种网络浏览器的技术规范,它为Web服务器定义了一种方式,允许网页从不同的域访问其资源。而这种访问是被同源策略所禁止的。CORS系统定义了一种浏览器和服务器交互的方式来确定是否允许跨域请求。

通过阅读我们知道,当我们进行跨越请求的时候,因为同源策略的限制,如果访问跨域请求时,跨源资源共享(CORS)机制为web服务器跨域访问控制提供了安全的跨域数据传输。

使用CORS的方式非常简单,但是需要同时对前端和服务器端做相应处理。

1、 前端
客户端使用XmlHttpRequest发起Ajax请求,当前绝大部分浏览器已经支持CORS方式,且主流浏览器均提供了对跨域资源共享的支持。

2、 服务器端
如果服务器端未做任何配置,则前端发起Ajax请求后,会得到CORS Access Deny,即跨域访问被拒绝。

对于C#做如下配置可允许资源的跨域访问:

<system.webServer>
...
<httpProtocol>
<customHeaders>
<add name="Access-Control-Allow-Origin" value="*" />
<add name="Access-Control-Allow-Headers" value="Origin, X-Requested-With, Content-Type" />
<add name="Access-Control-Allow-Methods" value="PUT,GET,POST,DELETE,OPTIONS"/>
</customHeaders>
</httpProtocol>
</system.webServer>

对于nodejs做如下配置可允许资源的跨域访问:

//设置CORS跨域访问
app.all('*', function (req, res, next) {
res.header("Access-Control-Allow-Origin", "*");
res.header("Access-Control-Allow-Headers", "X-Requested-With, accept, origin, content-type");
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();
});

Access-Control-Allow-Origin:*表示允许任何域发起请求,如果只允许特定的域访问,则设置Access-Control-Allow-Origin:xxx为具体域名即可。

关键词: Options
OPTIONS请求旨在发送一种“探测”请求以确定针对某个目标地址的请求必须具有怎样的约束(比如应该采用怎样的HTTP方法以及自定义的请求报头),然后根据其约束发送真正的请求。比如针对“跨域资源”的预检(Preflight)请求采用的HTTP方法就是OPTIONS。

简而言之,OPTIONS请求方法的主要用途有两个:

1、获取服务器支持的HTTP请求方法;

2、用来检查服务器的性能。

CORS 预检请求

CORS 通过一种叫做 Preflighted Requests 的透明服务器验证机制支持开发人员使用自定义的头部、GET 或者 POST和 HEAD 之外的方法,以及不同类型的主体内容。

简单请求:

意思就是,当满足某些条件时,不会触发 CORS 的预检请求,这种请求我们称之为「简单请求」,它需要满足以下条件:

使用以下任一方法:

GET
HEAD
POST

只能使用以下头部字段 :
Fetch 规范把它们定义为 对 CORS 安全的首部字段集合

 Accept 
 Accept-Language 
 Content-Language 
 Content-Type (需要注意额外的限制) 
 DPR 
 Downlink 
 Save-Data 
 Viewport-Width
 Width

Content-Type 的值仅限于下列三者之一:

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

请求中的任意 XMLHttpRequestUpload 对象均没有注册任何事件监听器;
XMLHttpRequestUpload 对象可以使用 XMLHttpRequest.upload 属性访问。

请求中没有使用 ReadableStream 对象。

满足以上条件就是简单请求。
简单请求
对于简单请求,浏览器直接发出CORS请求。具体来说,就是在头信息之中,增加一个Origin字段。

Origin字段用来说明,本次请求来自哪个源(协议 + 域名 + 端口)。服务器根据这个值,决定是否同意这次请求。

如果Origin指定的源,不在许可范围内,服务器会返回一个正常的HTTP回应。浏览器发现,这个回应的头信息没有包含Access-Control-Allow-Origin字段(详见下文),就知道出错了,从而抛出一个错误,被XMLHttpRequest的onerror回调函数捕获。注意,这种错误无法通过状态码识别,因为HTTP回应的状态码有可能是200。

如果Origin指定的域名在许可范围内,服务器返回的响应,会多出几个头信息字段。都以Access-Control- 开头:

(1)Access-Control-Allow-Origin

该字段是必须的。它的值要么是请求时Origin字段的值,要么是一个*,表示接受任意域名的请求。

需要注意的是,如果要发送Cookie,Access-Control-Allow-Origin就不能设为星号,必须指定明确的、与请求网页一致的域名。同时,Cookie依然遵循同源政策,只有用服务器域名设置的Cookie才会上传,其他域名的Cookie并不会上传,且(跨源)原网页代码中的document.cookie也无法读取服务器域名下的Cookie。

(2)Access-Control-Allow-Credentials

该字段可选。它的值是一个布尔值,表示是否允许发送Cookie。默认情况下,Cookie不包括在CORS请求之中。设为true,即表示服务器明确许可,Cookie可以包含在请求中,一起发给服务器。这个值也只能设为true,如果服务器不要浏览器发送Cookie,删除该字段即可。

(3)Access-Control-Expose-Headers

该字段可选。CORS请求时,XMLHttpRequest对象的getResponseHeader()方法只能拿到6个基本字段:Cache-Control、Content-Language、Content-Type、Expires、Last-Modified、Pragma。如果想拿到其他字段,就必须在Access-Control-Expose-Headers里面指定。

非简单请求

非简单请求是那种对服务器有特殊要求的请求,比如请求方法是PUT或DELETE,或者Content-Type字段的类型是application/json。

我工作中写的所有页面拉的接口都是非简单请求。
在这里插入图片描述
非简单请求的CORS请求,会在正式通信之前,增加一次HTTP查询请求,称为"预检"请求(preflight)。

浏览器先询问服务器,当前网页所在的域名是否在服务器的许可名单之中,以及可以使用哪些HTTP动词和头信息字段。只有得到肯定答复,浏览器才会发出正式的XMLHttpRequest请求,否则就报错。

在页面域名与接口域名不一致的情况下,就出现了每次请求前先发送一个options请求的问题。

OPTIONS请求头信息中,除了Origin字段,还至少会多两个特殊字段:

(1)Access-Control-Request-Method

该字段是必须的,用来列出浏览器的CORS请求会用到哪些HTTP方法。

(2)Access-Control-Request-Headers

该字段是一个逗号分隔的字符串,指定浏览器CORS请求会额外发送的头信息字段。

在这里插入图片描述
服务器收到预检请求后,检查了Origin、Access-Control-Request-Method和Access-Control-Request-Headers字段以后,确认允许跨源请求,就可以做出回应。

上面的HTTP回应中,关键的是Access-Control-Allow-Origin字段,表示http://lizard.qa.nt.ctripcorp.com可以请求数据。该字段也可以设为星号,表示同意任意跨源请求。

如果浏览器否定了"预检"请求,会返回一个正常的HTTP回应,但是没有任何CORS相关的头信息字段。这时,浏览器就会认定,服务器不同意预检请求,因此触发一个错误,被XMLHttpRequest对象的onerror回调函数捕获。控制台会打印出如下的报错信息。

XMLHttpRequest cannot load http://lizard.qa.nt.ctripcorp.com
Origin http://lizard.qa.nt.ctripcorp.com is not allowed by Access-Control-Allow-Origin.

其他字段中Access-Control-Max-Age 用来指定本次预检请求的有效期,单位为秒。该字段可选。

下面两个例子:

const xhr = new XMLHttpRequest();
        xhr.onreadystatechange = () => {
            if (xhr.readyState === 4) {
                if (xhr.status >= 200 && xhr.status < 300 || xhr.status === 304) {
                    document.body.innerText = xhr.responseText;
                } else {
                    console.log('Request failed: ', xhr.status);
                }
            }
        }
        xhr.open('POST', 'http://127.0.0.1:3000/message', true);
        xhr.send(null);
// Request Headers
Accept: */*
Accept-Encoding: gzip, deflate, br
Accept-Language: en,zh-CN;q=0.9,zh;q=0.8,zh-TW;q=0.7,fr;q=0.6,zu;q=0.5,ja;q=0.4
Connection: keep-alive
Content-Length: 0
Host: 127.0.0.1:3000
Origin: http://127.0.0.1:4000
Referer: http://127.0.0.1:4000/
Sec-Fetch-Mode: cors
Sec-Fetch-Site: same-site
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/78.0.3904.97 Safari/537.36

在上面的代码中,POST 方法符合规则,是一个简单的请求,所以不会发起预检请求。 不符合简单请求条件的请求我们称为「需预检的请求」。假设我们给刚才的请求加上一个额外的头部 Content-Type ,它不属于 text/plain multipart/form-data application/x-www-form-urlencoded 三者之一,在发起真正请求之前会先发送一个 OPTIONS 预检请求到服务器。

const xhr = new XMLHttpRequest();
        xhr.onreadystatechange = () => {
            if (xhr.readyState === 4) {
                if (xhr.status >= 200 && xhr.status < 300 || xhr.status === 304) {
                    document.body.innerText = xhr.responseText;
                } else {
                    console.log('Request failed: ', xhr.status);
                }
            }
        }
        xhr.open('POST', 'http://127.0.0.1:3000/message', true);
        xhr.setRequestHeader("Content-Type", "application/json");
        xhr.send(null);

OPTIONS 请求头部:

Accept: */*
Accept-Encoding: gzip, deflate, br
Accept-Language: en,zh-CN;q=0.9,zh;q=0.8,zh-TW;q=0.7,fr;q=0.6,zu;q=0.5,ja;q=0.4
Access-Control-Request-Headers: content-type // new header
Access-Control-Request-Method: POST // new header
Connection: keep-alive
Host: 127.0.0.1:3000
Origin: http://127.0.0.1:4000
Referer: http://127.0.0.1:4000/
Sec-Fetch-Mode: cors
Sec-Fetch-Site: same-site
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/78.0.3904.97 Safari/537.36

可以看到,此时浏览器会在 OPTIONS 请求中发送两个往外的请求,服务器收到请求后决定是否允许该类型的请求,在响应中发送以下头部进行沟通:

Access-Control-Allow-Origin: 与简单的请求相同
Access-Control-Allow-Methods: 允许的方法,以逗号隔开
Access-Control-Allow-Headers: 允许头部,多个头部以逗号隔开
Access-Control-Max-Age: 这个 OPTIONS 请求缓存多久(以秒表示)

以下是一段简单的使用 express 的处理代码:

const express = require('express');
const app = express();
const WHITE_LIST = ['http://127.0.0.1:4000']
app.use((req, res, next) => {
    const { origin } = req.headers;
    console.log('options headers', req.headers);
    if (WHITE_LIST.includes(origin)) {
        const headers = {
            'Access-Control-Allow-Origin': origin,
            'Access-Control-Allow-Methods': '*'
        }
        const reqHeader = req.headers['access-control-request-headers'];
        if (reqHeader) {
            headers['Access-Control-Allow-Headers'] = reqHeader;
        }
        res.set(headers);
        if (req.method === 'OPTIONS') {
            res.end();
        }
    }
    next();
})

要注意的是,简单请求和预检请求的主要区别在于,是否要发送额外的 OPTIONS 请求来检验服务器是否支持发送的方法或者自定义头部。

关键词: Preflighted Requests 预检请求

Preflighted Requests是CORS中一种透明服务器验证机制。预检请求首先需要向另外一个域名的资源发送一个 HTTP OPTIONS 请求头,其目的就是为了判断实际发送的请求是否是安全的。

下面的2种情况需要进行预检:

非简单请求,比如使用Content-Type 为 application/xml 或 text/xml 的 POST 请求;(什么是简单请求,什么是非简单请求,请移步阮一峰的跨域资源共享 CORS 详解

总结

规范要求,对那些可能对服务器数据产生副作用的 HTTP 请求方法(特别是 GET 以外的 HTTP 请求,或者搭配某些 MIME 类型的 POST 请求),浏览器必须首先使用 OPTIONS 方法发起一个预检请求(preflight request),从而获知服务端是否允许该跨域请求。服务器确认允许之后,才发起实际的 HTTP 请求。

“需预检的请求”要求必须首先使用 OPTIONS 方法发起一个预检请求到服务器,以获知服务器是否允许该实际请求。

当请求满足下述任一条件时,即应首先发送预检请求(使用OPTIONS):

1、使用了下面任一 HTTP 方法:

PUT
DELETE
CONNECT
OPTIONS
TRACE
PATCH

2、人为设置了对 CORS 安全的首部字段集合之外的其他首部字段。该集合为:

Accept
Accept-Language
Content-Language
Content-Type (but note the additional requirements below,需要注意额外的限制)
DPR
Downlink
Save-Data
Viewport-Width
Width

3、Content-Type 的值不属于下列之一:

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

https://cloud.tencent.com/developer/article/1046663


https://developer.mozilla.org/zh-CN/docs/Web/HTTP/Access_control_CORS


https://zhuanlan.zhihu.com/p/31935253

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值