前端无法直接实现cors预检处理,必须由后端正确响应options请求;content-type非简单值会触发预检,后端需返回access-control-allow-headers;spring boot 2.4+需配置corsconfigurationsource;nginx需条件判断origin并显式return 204。

前端无法直接“实现”CORS预检请求的处理——OPTIONS 预检是浏览器自动发起、必须由后端响应的环节,HTML 本身不参与逻辑处理。所有所谓“HTML 实现”,本质是前端触发了预检,而真正要解决的是后端如何正确响应它。
为什么在 HTML 或 fetch 中改个 Content-Type 就挂了
因为 Content-Type: application/json 不属于简单请求的三类合法值(text/plain / application/x-www-form-urlencoded / multipart/form-data),浏览器立刻判定为非简单请求,强制先发 OPTIONS。如果你的后端没监听或没返回 Access-Control-Allow-Headers: Content-Type,预检就失败,主请求压根不会发出——你在 Network 面板里只看到一个红字 OPTIONS 404 或 500,fetch 报错却是 No 'Access-Control-Allow-Origin' header,其实后端连主请求的影子都没见到。
常见触发点包括:
fetch('/api/user', { method: 'POST', headers: { 'Content-Type': 'application/json' } })-
axios.post('/login', { token: 'xxx' })(默认带application/json) - 任何带自定义 header 的请求,比如
'X-Auth-Token': 'abc'
Spring Boot 2.4+ 的 @CrossOrigin 为什么突然不生效
新版本默认把 OPTIONS 路径从 CORS 过滤链中剥离了,@CrossOrigin 只作用于实际接口路径(如 /api/delete),但预检请求打到的是同路径的 OPTIONS /api/delete,若没显式配置,就会 405 Method Not Allowed。
必须用配置类替代注解:
@Bean
CorsConfigurationSource corsConfigurationSource() {
CorsConfiguration config = new CorsConfiguration();
config.setAllowedOrigins(Arrays.asList("http://localhost:3000"));
config.setAllowCredentials(true);
config.addAllowedMethod("DELETE");
config.addAllowedMethod("PUT");
config.addExposedHeader("X-Total-Count");
config.setMaxAge(3600L);
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/**", config);
return source;
}
另外两个关键点:
- 如果用了
SpringSecurity,必须在SecurityConfig里调用http.cors(),否则安全过滤器会提前拦截OPTIONS -
allowedOrigins不能写"*"+allowCredentials(true),否则启动报错
Nginx 层加 CORS 头时,OPTIONS 响应容易漏掉 Origin 判断
Nginx 不像应用层能自然读取请求头,它对 Origin 的判断是字符串匹配,且预检请求可能不带 Origin(极少见但存在),直接无条件加头会导致缓存污染或安全风险。
正确写法必须加条件判断:
location /api/ {
if ($http_origin ~* ^(https?://localhost:3000|https://myapp.com)$) {
add_header 'Access-Control-Allow-Origin' $http_origin;
add_header 'Access-Control-Allow-Methods' 'GET, POST, OPTIONS, DELETE';
add_header 'Access-Control-Allow-Headers' 'Content-Type, X-Auth-Token';
add_header 'Access-Control-Allow-Credentials' 'true';
add_header 'Vary' 'Origin';
}
if ($request_method = 'OPTIONS') {
add_header 'Access-Control-Max-Age' 3600;
add_header 'Access-Control-Allow-Credentials' 'true';
add_header 'Access-Control-Allow-Headers' 'Content-Type, X-Auth-Token';
add_header 'Access-Control-Allow-Methods' 'GET, POST, OPTIONS, DELETE';
add_header 'Content-Length' 0;
add_header 'Content-Type' 'text/plain; charset=utf-8';
return 204;
}
}
注意:add_header 在 if 块内才生效;return 204 必须明确,不能只靠 add_header;Vary: Origin 是为了防止 CDN 或代理缓存错误的跨域响应。
Access-Control-Max-Age 设太高反而埋雷
设成 3600(1小时)看似减少预检,但一旦你上线新策略(比如新增了允许的 header),浏览器会继续用旧缓存的预检结果,直到过期——这期间所有相关请求都会失败,且开发者很难定位是缓存问题。
更稳妥的做法:
- 开发阶段设为
60(1分钟),快速验证 - 上线后根据接口稳定性逐步提高,比如
86400(24小时)适合长期不变的公开 API - 只要后端支持动态 origin 白名单,就别依赖长缓存,优先保证可维护性
真正难的从来不是加几个响应头,而是理解预检不是“多一次请求”,而是浏览器在替你做安全协商——漏掉任一环,它就拒绝帮你发下一条。
前端入门到VUE实战笔记:立即使用
在学习笔记中,你将探索 前端 的入门与实战技巧!











