导读:本期聚焦于Amelis创作的《什么是预检请求Preflight?跨域场景下浏览器为何多发一次OPTIONS请求》,敬请观看详情。前端调试接口时,在Network面板里常常会看到一个_method为OPTIONS的请求先于真实请求发出,这就是预检请求Preflight。它的出现让不少刚接触跨域的开发者困惑:为什么我的POST请求变成了OPTIONS?服务器又该如何响应?本文从浏览器同源策略讲起,解释CORS机制中简单请求与非简单请求的区别,说明哪些条件会触发预检,例如自定义头部、Content-Type为application/json、PUT或DELETE方法等。同时给出Nginx和后端服务处理OPTIONS请求的完整配置示例,并分析预检请求对性能的影响以及缓存策略的配置方法,帮助开发者彻底理解并正确处理跨域预检问题。

在前后端分离的架构下,跨域请求几乎无法避免。不少开发者在浏览器控制台看到过类似“Response to preflight request doesn't pass access control check”的报错,或者在Network面板里发现真实请求发出之前,多了一个方法为OPTIONS的神秘请求。这个请求就是预检请求,英文叫Preflight Request。要理解它为什么存在、什么时候触发、服务端怎么正确响应,需要先从浏览器的同源策略和CORS机制说起。

什么是预检请求Preflight?跨域场景下浏览器为何多发一次OPTIONS请求

同源策略与CORS的基本原理

浏览器出于安全考虑,实行同源策略:协议、域名、端口三者只要有一个不同,就被视为跨域。跨域时浏览器默认会限制页面脚本读取响应内容。但这并不意味着请求发不出去,而是请求发出后,响应被浏览器拦截,脚本拿不到数据。

为了在保证安全的前提下允许合法的跨域访问,W3C提出了CORS(Cross-Origin Resource Sharing,跨域资源共享)标准。它的核心思路是:浏览器在跨域请求中自动附加一些头部信息,例如Origin表示请求来源,服务端通过返回Access-Control-Allow-Origin等头部声明自己允许哪些来源访问,浏览器据此判断是否把响应放行给页面脚本。

关键在于,这个校验过程有两种模式:一种是直接发真实请求,浏览器拿到响应后检查头部,这就是简单请求模式;另一种是先发一个“试探性”的OPTIONS请求,确认服务端允许之后再发真实请求,这就是预检请求模式。预检的作用相当于浏览器替开发者先问一句:“我接下来要用PUT方法、带自定义头X-Token访问这个接口,你允许吗?”服务端明确同意后,真实请求才会发出。

哪些请求会触发预检:简单请求与非简单请求的界限

CORS规范中把不会触发预检的请求称为简单请求(Simple Request),它必须同时满足几个条件:请求方法是GET、HEAD或POST;请求头只能包含AcceptAccept-LanguageContent-LanguageContent-Type等少数几个字段,且Content-Type仅限于text/plainapplication/x-www-form-urlencodedmultipart/form-data三种;请求中没有注册ReadableStream对象等额外限制。

只要任何一条不满足,浏览器就会先发送预检请求。日常开发中最常见的触发场景有三个:第一,前后端交互普遍使用JSON格式,而Content-Type: application/json不属于简单请求允许的类型;第二,携带自定义头部,例如鉴权用的AuthorizationX-Token;第三,使用了PUT、DELETE、PATCH等非简单方法。这也解释了为什么RESTful接口几乎每次都要面对预检。

预检请求本身是一个方法为OPTIONS的HTTP请求,浏览器会自动附带两个重要头部:Access-Control-Request-Method告知真实请求将使用的方法,Access-Control-Request-Headers列出真实请求会携带的非简单头部。注意预检请求是浏览器行为,前端代码无法跳过或取消它,任何试图在业务代码里“不发预检”的做法都是徒劳的。

服务端如何正确响应预检请求

服务端收到OPTIONS请求后,需要在响应中返回对应的CORS头部,告诉浏览器允许的操作范围。核心的响应头包括:Access-Control-Allow-Origin指定允许的来源,可以是具体的域名或者*,但如果请求携带Cookie,则必须返回具体域名且不能是*Access-Control-Allow-Methods列出允许的方法;Access-Control-Allow-Headers列出允许的请求头,必须覆盖预检请求中Access-Control-Request-Headers声明的所有头部;Access-Control-Allow-Credentials设为true表示允许携带Cookie。

以Nginx为例,一个典型的跨域配置如下:

location /api/ {
    add_header Access-Control-Allow-Origin $http_origin;
    add_header Access-Control-Allow-Methods "GET, POST, PUT, DELETE, OPTIONS";
    add_header Access-Control-Allow-Headers "Content-Type, Authorization, X-Token";
    add_header Access-Control-Allow-Credentials "true";

    # OPTIONS 预检请求直接返回 204,不转发到后端
    if ($request_method = OPTIONS) {
        return 204;
    }

    proxy_pass http://127.0.0.1:8080;
}

后端框架的处理方式类似。以Spring Boot为例,可以通过全局配置类优雅地处理跨域和预检:

@Configuration
public class CorsConfig implements WebMvcConfigurer {
    @Override
    public void addCorsMappings(CorsRegistry registry) {
        registry.addMapping("/**")
                .allowedOriginPatterns("*")
                .allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS")
                .allowedHeaders("*")
                .allowCredentials(true)
                .maxAge(3600);
    }
}

这里有个容易踩的坑:有些后端接口对OPTIONS请求做了鉴权拦截,直接返回401,导致预检失败,真实请求根本发不出去。排查时要注意区分是预检失败还是业务请求失败,看Network面板中OPTIONS请求的状态码即可。预检请求不会携带业务自定义头的实际值,也不应该参与登录态校验,正确做法是在过滤器中把OPTIONS请求放行。

预检带来的性能开销与缓存优化

预检请求本质上多了一次完整的网络往返,对于接口密集的应用来说,每个请求前都先来一次OPTIONS,延迟会明显增加。尤其在移动端弱网环境下,这个开销不可忽视。好在CORS规范提供了预检缓存机制:服务端在预检响应中返回Access-Control-Max-Age头部,告诉浏览器这次预检的结果可以缓存多少秒。在缓存有效期内,同一来源对同一URL的相同类型请求不会再触发预检。

上面Spring Boot配置中的maxAge(3600)就是设置缓存一小时。需要注意的是,不同浏览器对最大缓存时间有上限,Chrome上限是两小时,Firefox曾经是二十四小时,超出上限的值会被截断。此外,缓存是以“来源+目标URL+请求条件”为粒度的,任何一项变化都会导致缓存失效重新预检。

如果应用对性能极其敏感,还可以从架构层面减少预检:比如让前端请求代理到同源服务(开发环境用webpack或Vite的proxy配置,生产环境用Nginx反向代理),跨域问题在网关层解决,浏览器视角里就是同源请求,自然不存在预检。这种方式在前后端分离项目中非常常见,也是生产环境推荐的方案之一。

常见问题排查思路

遇到跨域报错时,建议按以下顺序排查:先确认Network面板中是否真的发出了OPTIONS预检请求,以及它的响应状态码和响应头;再核对Access-Control-Allow-Origin与页面实际来源是否完全一致,注意协议和端口也要匹配;然后检查Access-Control-Allow-Headers是否漏掉了请求中使用的自定义头;最后如果请求携带了Cookie,确认Access-Control-Allow-Credentialstrue且Origin不是*

还有一个细节值得注意:某些开发者发现设置Access-Control-Allow-Origin: *后仍然报错,多半是因为请求中带了withCredentials,浏览器规范明确禁止通配符与凭证模式同时使用。理解了预检请求的完整流程和这些细节,跨域问题就不再是黑盒,排查起来会有清晰的思路。

Preflight跨域CORS修改时间:2026-09-06 21:02:41

免责声明:已尽一切努力确保本网站所含信息的准确性。网站作品多为原创整理与精心创作,观点力求客观中立。本站旨在免费分享,内容仅供个人学习、研究或参考使用。若引用了第三方作品,版权归原作者所有。如内容涉及您的权益,请联系我们进行处理Email:chomcom@qq.com。
引用或转载本作品时,请注明当前出处:https://www.ipipp.com/html/20260906/51794.html,基于非商业用途的前提下,欢迎转载或二创本作品。
内容垂直聚焦
专注技术核心技术栏目,确保每篇文章深度聚焦于实用技能。从代码技巧到架构设计,为用户提供无干扰的纯技术知识沉淀,精准满足专业提升需求。
知识结构清晰
覆盖从开发到部署的全链路。AI、前端、编程、数据库、服务器、建站、系统层层递进,构建清晰学习路径,帮助用户系统化掌握开发与运维所需的核心技术。
深度技术解析
拒绝泛泛而谈,深入技术细节与实践难点。无论是数据库优化还是服务器配置,均结合真实场景与代码示例进行剖析,致力于提供可直接应用于工作的解决方案。
专业领域覆盖
精准对应开发生命周期。从前端界面到后端编程,从数据库操作到服务器运维,形成完整闭环,一站式满足全栈工程师和运维人员的技术需求。
即学即用高效
内容强调实操性,步骤清晰、代码完整。用户可根据教程直接复现和应用于自身项目,显著缩短从学习到实践的距离,快速解决开发中的具体问题。
持续更新保障
专注既定技术方向进行长期、稳定的内容输出。确保各栏目技术文章持续更新迭代,紧跟主流技术发展趋势,为用户提供经久不衰的学习价值。