CORS
这一课你将能做到
- 用「协议 + 域名 + 端口」三要素定义源(Origin),并快速判断任意两串 URL 是否跨源
- 区分简单请求与预检 OPTIONS,列出至少三种会触发预检的特征,并读懂
Access-Control-Allow-* 四个响应头
- 遇到 Console 里的 CORS 报错时,到 Network 定位失败的 OPTIONS/主请求,判断该改 Spring 哪处配置,还是该改成同域反代
为什么要学
本地前端 localhost:3000 调后端 localhost:8080,或者前后端挂在不同子域,浏览器会直接拦截「读跨源响应」——Console 一串红色 blocked by CORS policy,新手最容易在这里耗一整天:改前端、换请求库、装禁用 CORS 的插件,全都不对。
根因永远是服务端 CORS 配置(或干脆同域部署)。面试常问「为什么 curl 没有跨域问题」「CORS 预检是什么」——本质考察你是否理解:CORS 是浏览器的安全机制,不是服务器拒绝了你。这一课学完,跨域问题从玄学变成流程。
先建立图像
源 = 协议 + 域名 + 端口,三者全同才算同源:
https://coderoadmap.cn vs https://api.coderoadmap.cn → 跨源(域名不同)
http://localhost:3000 vs http://localhost:8080 → 跨源(端口不同)
https://a.com vs http://a.com → 跨源(协议不同)
https://coderoadmap.cn/x vs https://coderoadmap.cn/y → 同源(路径不算)
http://127.0.0.1:3000 vs http://localhost:3000 → 跨源(127.0.0.1 ≠ localhost)
两类请求的处理方式:
简单请求:直接发出 → 浏览器看响应头有没有 CORS 允许 → 决定 JS 能不能读
预检请求:先发 OPTIONS「我能不能发这种请求?」→ 通过后才发真正的 GET/POST/PUT…
触发预检的三类典型特征:
| 特征 | 例子 |
|---|
| 方法非简单方法(GET/HEAD/POST) | PUT、DELETE、PATCH |
| Content-Type 非简单值 | application/json(表单三兄弟才是简单值) |
| 带了自定义头 | Authorization、X-Request-Id |
服务器预检/响应里应出现(按需):
Access-Control-Allow-Origin: http://localhost:3000
Access-Control-Allow-Methods: GET, POST, PUT, DELETE, OPTIONS
Access-Control-Allow-Headers: Content-Type, Authorization
Access-Control-Allow-Credentials: true
红线:Allow-Origin: * 与 Allow-Credentials: true 互斥——带 Cookie 的跨源必须回显具体 Origin。
跟着做
步骤 1:给 Spring Boot 配一段真实可用的 CORS
@Configuration
public class CorsConfig implements WebMvcConfigurer {
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/api/**")
// 生产写白名单;本地开发放 3000
.allowedOrigins("http://localhost:3000")
.allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS")
.allowedHeaders("Content-Type", "Authorization")
.allowCredentials(true) // 与具体 Origin 配对,不能配 "*"
.maxAge(3600); // 预检结果缓存 1 小时,减少 OPTIONS
}
}
重启后,localhost:3000 的前端调 /api/** 即可跨源工作。
步骤 2:用 curl 模拟一次预检
浏览器才会自动发 OPTIONS,curl 可以替浏览器问一次:
curl -i -X OPTIONS http://localhost:8080/api/notes \
-H "Origin: http://localhost:3000" \
-H "Access-Control-Request-Method: GET" \
-H "Access-Control-Request-Headers: authorization"
期望响应:200 或 403 之外的正常状态 + 上面四个 Access-Control-* 头。把 Origin 换成 http://evil.com 再试——白名单外应看不到 Allow-Origin 头,这就是拦截机制。
步骤 3:亲手制造一次 CORS 失败并读懂它
临时把配置里的 allowedOrigins 注释掉(或改成别的端口),然后:
- 本地起一个
:3000 页面(或直接在 Console 执行跨源 fetch)
- Console 出现
has been blocked by CORS policy: No 'Access-Control-Allow-Origin' header is present
- Network 面板:先找 OPTIONS 请求(可能 403/404/无 CORS 头),再看主请求
关键认知:主请求可能已经成功到达服务器并返回 200(看 Spring 日志),只是浏览器不让 JS 读结果——服务器日志有记录、前端却在报 CORS,就是这么来的。
步骤 4:带 Cookie 的跨源三件套
// 前端:显式要求带凭证
fetch('http://localhost:8080/api/auth/me', {
credentials: 'include',
headers: { 'Content-Type': 'application/json' }
})
服务端必须同时满足(Cookie 课的「卡点急救 3」在此展开):
allowedOrigins 回具体 Origin(不许 *)
allowCredentials(true) → 响应带 Access-Control-Allow-Credentials: true
- Cookie 自身的 SameSite/Domain 允许该跨站场景
步骤 5:生产同域反代——从根上消灭 CORS
浏览器只访问 https://coderoadmap.cn(同源!)
Nginx:
/api/ → proxy_pass http://127.0.0.1:8080
/ → 前端页面服务
→ 页面与 API 同源 → 浏览器根本不启动 CORS 检查
这是本站生产架构:开发期跨源要配 CORS,生产尽量同域。CORS 当开发与多域场景的工具,而不是永久架构负担。
步骤 6:排除「假 CORS」
有时真凶是 500/502:异常响应(HTML 错误页)没有 CORS 头,浏览器报 CORS——但它其实是上游挂了。排错二分法:
# 绕过浏览器看裸响应:状态码与 Content-Type 一目了然
curl -i -X OPTIONS http://localhost:8080/api/notes -H "Origin: http://localhost:3000" \
-H "Access-Control-Request-Method: GET"
curl -i http://localhost:8080/api/notes
curl 显示 5xx → 先修服务端;curl 显示 200 但无 Allow-Origin 头 → 才是真的 CORS 配置问题。
步骤 7:过滤器全局方案(对照记忆)
网关/安全框架场景常用 CorsFilter(优先级要高于鉴权过滤器,否则 OPTIONS 先被 401):
@Bean
public CorsFilter corsFilter() {
CorsConfiguration config = new CorsConfiguration();
config.setAllowedOrigins(List.of("http://localhost:3000"));
config.setAllowedMethods(List.of("GET", "POST", "PUT", "DELETE", "OPTIONS"));
config.setAllowedHeaders(List.of("Content-Type", "Authorization"));
config.setAllowCredentials(true);
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/**", config);
return new CorsFilter(source);
}
两种方案别混用同一组路径——头重复或覆盖会让预检结果时好时坏,极难排查。
讲清楚要点
- 因为同源策略保护的是用户,所以「别的网站的 JS」默认不能读你的响应——CORS 是服务器签发的通行证:决定权在响应头,不在前端代码。
- 因为预检发生在真请求之前,所以只放行了 GET 却忘了 OPTIONS/自定义头,会表现为「预检失败、主请求根本没发」——
allowedMethods 和 allowedHeaders 要覆盖前端实际会用的组合。
- 因为 curl/Postman 没有同源策略,所以「curl 通、浏览器不通」优先查 CORS 与 Cookie——这不是玄学,是两套运行环境的安全差异。
- 因为
* 与 credentials 互斥,所以带 Cookie 的 SPA 必须回显具体 Origin——要么维护白名单,要么同域反代。
- 因为自定义头和 JSON Content-Type 几乎必触发预检,所以开发期 Network 里 OPTIONS 多是正常现象——
maxAge 可以减少 OPTIONS 频率,但配置变更后要让浏览器缓存过期。
- 因为同域反代从架构上消灭跨源,所以能合并域名时优先合并——Nginx 也能直接加 CORS 头,但业务白名单通常放应用层,两层都配容易互相覆盖。
你可能会懵的地方
-
CORS 报错是不是服务器拒绝了请求?
不一定,而且经常不是。主请求可能已到达并 200,只是响应缺 Allow-Origin 头,浏览器不让 JS 读 body。判断方法:看服务器日志有没有这次请求的处理记录。
-
mode: 'no-cors' 能当修复手段吗?
不能。它把请求降级为「不透明响应」,JS 完全读不到内容,只能用于图片、beacon 这类「发出去就行」的场景。跨域要读数据,正路是配 CORS 或同域反代。
-
为什么 Postman/后端服务间调用从来不报 CORS?
同源策略是浏览器行为。服务器之间、curl、原生 App 都不受限。所以「后端说接口好的,前端说跨域」时,两边说的都是真话。
-
Allow-Origin: * 到底能不能用?
纯公开只读数据(如公开课表)可以接受。涉及用户身份/Cookie/私有数据时危险——任何网站都能借用户浏览器读你的接口,且与 credentials 互斥。生产建议显式白名单。
-
预检 OPTIONS 请求打到后端,被 Spring Security 401 了怎么办?
预检请求不带凭证,不能要求鉴权。把 CORS 处理放在认证过滤器之前(CorsFilter 高优先级),或对 OPTIONS 放行:.requestMatchers(HttpMethod.OPTIONS).permitAll()。
错法对照
| 错法 | 问题 | 正解 |
|---|
前端改 mode: 'no-cors' 当修复 | 响应变不透明,JS 读不到任何数据 | 服务端配 CORS 或改同域反代 |
Allow-Origin: * + Allow-Credentials: true | 规范互斥,浏览器直接判失败 | 具体 Origin 回显 + credentials |
CORS 只配了 allowedMethods("GET") | JSON POST/PUT 的预检挂掉,接口「假死」 | 方法、头、OPTIONS 按实际需求放全 |
| 开发期装浏览器插件「禁用 CORS」 | 掩盖问题,上线即复发,还引入安全风险 | 本地正确配置白名单或走 dev proxy 同域化 |
| 接口 500 前端却报 CORS,修了半天 CORS | 错误页无 CORS 头,浏览器归因到 CORS | curl 先看真实状态码,5xx 先修服务端 |
卡点急救
- Console 报
No 'Access-Control-Allow-Origin' header:Network 里点开该请求看 Response Headers 有没有 CORS 头。没有 → 检查 Spring 配置是否生效(配置类有没有被扫描、路径是否匹配);换 Origin 为白名单内地址再 curl 一次对比。
- 预检 OPTIONS 返回 401/403:CORS 处理被安全过滤器抢在了前面。用 CorsFilter 注册并设最高优先级,或 Security 配置里对 OPTIONS permitAll。
- 网关/nginx 层和应用层都配了 CORS,头重复冲突:
Access-Control-Allow-Origin 出现两次浏览器直接报错。只留一层:业务白名单放应用,网关层删掉 CORS 相关 add_header;或反指定一层统一处理。
面试常问
-
什么是同源策略?CORS 又是什么?
同源策略:浏览器限制「脚本只能读同源响应」,防止恶意网站借用户身份读别的站的数据。CORS(跨源资源共享):服务器通过一组 Access-Control-* 响应头,显式授权指定源的页面读取自己的资源——是同源策略的标准化豁免机制。关键:这是浏览器行为,curl/服务端调用不受限。
-
什么情况下浏览器会发预检 OPTIONS 请求?
非简单请求:方法不是 GET/HEAD/POST;或 Content-Type 不是 application/x-www-form-urlencoded、multipart/form-data、text/plain 之一;或带了自定义头(如 Authorization)。预检通过后浏览器才发真实请求,结果可被 Access-Control-Max-Age 缓存。
-
生产上如何彻底避免 CORS 问题?
同域部署:Nginx 把页面与 /api 反代到同一域名下,浏览器全程同源。无法同域时维护 Origin 白名单 + 按需 credentials,并保证预检处理在鉴权之前。避免双层配 CORS 头。
自测三问
- 判断
http://127.0.0.1:3000 与 http://localhost:3000 是否同源,为什么?
- 列出三种触发预检的请求特征,并说出前端哪种常用写法「必然」触发预检。
- 生产同域 Nginx 反代如何消灭 CORS?开发期仍跨源时,最小正确的 Spring 配置长什么样?
小结一句话
CORS 是浏览器读跨源响应前要拿的通行证:OPTIONS 预检先问可不可以——根治靠服务端正确响应头或同域反代,不靠关掉浏览器安全。
延伸