一句话先说结论:CORS 报错通常不是后端数据出问题,而是浏览器在拦——服务器返回了数据,但响应头上没有
Access-Control-Allow-Origin。Spring Boot 默认不加 CORS 头,必须显式配;最常见的翻车场景有两个:一是加了 Spring Security 后原来的 MVC CORS 配置“失效”,二是allowCredentials(true)和allowedOrigins("*")撞在一起被拒绝。
背景
CORS 是浏览器基于“同源策略”的放行机制:源(协议 + 域名 + 端口)不同就默认拦。前端跑在 http://localhost:3000,后端在 http://localhost:8080,两者端口不同就是跨域。跨域请求里,带自定义头、Content-Type: application/json、或用 PUT/DELETE 的,浏览器会先发一个 OPTIONS 预检请求,问服务器“你允不允许”。
这解释了为什么 curl/Postman 能通而浏览器报错——curl 不执行 CORS,浏览器才执行。
现象
前端控制台:
Access to XMLHttpRequest at 'http://localhost:8080/api/users'
from origin 'http://localhost:3000' has been blocked by CORS policy:
No 'Access-Control-Allow-Origin' header is present on the requested resource.
或者加了 Spring Security 之后,预检返回 401 且不带 CORS 头:
Cross-Origin Request Blocked: ... (Reason: CORS header
'Access-Control-Allow-Origin' missing). Status code: 401.
根因分析
三种主要的配置路径,选错或配漏都会失效:
@CrossOrigin(单控制器/方法,最快)WebMvcConfigurer.addCorsMappings(全局,只对 Spring MVC 生效)- Spring Security 的
http.cors()(集成 Security 时必须走这条)
最容易踩的是这几个坑:
坑 1:只在 yml 里写 spring.web.cors.* 不会生效。那些配置只是数据源,本身不注册任何过滤器或拦截器,纯 YAML 不等于开箱即用。
坑 2:加了 Spring Security 后,MVC 的 CORS 配置“突然失效”。原因是 Security 的过滤器链跑在控制器之前,OPTIONS 预检请求还没到 MVC 就被当成未认证请求拦下,返回 401 且不带 CORS 头。此时光有 WebMvcConfigurer 没用,必须在 Security 里同时打开 http.cors() 并放行 OPTIONS。
坑 3:allowCredentials(true) 与 allowedOrigins("*") 不能共存。带凭据(cookie/JWT)时浏览器要求 Access-Control-Allow-Origin 必须是具体源、不能是 *。Spring Boot 2.4+ 对这对组合会直接拒绝(启动就报 IllegalArgumentException)。要么列出具体 origin,要么改用 allowedOriginPatterns("*")。
坑 4:自定义 JWT Filter 拦截了预检。如果 OncePerRequestFilter 没对 OPTIONS 放行,预检流程会断在它手里。
解决方案
纯 Spring MVC:WebMvcConfigurer
@Configuration
public class WebConfig implements WebMvcConfigurer {
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/**")
.allowedOriginPatterns("http://localhost:3000")
.allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS")
.allowedHeaders("*")
.allowCredentials(true)
.maxAge(3600);
}
}
集成 Spring Security:SecurityFilterChain 里配
这才是“加了 Security 还跨域”的正解:
@Configuration
@EnableWebSecurity
public class SecurityConfig {
@Bean
public CorsConfigurationSource corsConfigurationSource() {
CorsConfiguration config = new CorsConfiguration();
config.setAllowedOriginPatterns(List.of("http://localhost:3000"));
config.setAllowedMethods(List.of("GET", "POST", "PUT", "DELETE", "OPTIONS"));
config.setAllowedHeaders(List.of("*"));
config.setAllowCredentials(true);
config.setMaxAge(3600L);
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/**", config);
return source;
}
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http.cors(cors -> cors.configurationSource(corsConfigurationSource()))
.authorizeHttpRequests(auth -> auth
.requestMatchers(HttpMethod.OPTIONS, "/**").permitAll() // 放行预检
.anyRequest().authenticated()
);
return http.build();
}
}
要点:http.cors(...) 显式挂上 CORS 源,并把 OPTIONS 预检 permitAll(),否则预检永远到不了 MVC。
关于 * 和凭据
- 不需要带 cookie/token:可以直接
allowedOrigins("*")。 - 需要带凭据:用
allowedOriginPatterns("*")或列出具体 origin,别用allowedOrigins("*")。
排查小抄
- 先看浏览器 Network:请求
Status = 0/ 标CORS error,是浏览器拦在了前面;返回4xx/5xx是已经到后端了。 - 看有没有
OPTIONS请求、它是否返回 200 且带 CORS 头。 curl能通、浏览器不通 → 就是 CORS/策略问题,不是接口写错。- 确认后端日志有没有请求进来,能判断是卡在浏览器还是卡在 Security。
小结
- CORS 是浏览器行为,后端要做的只是“把正确的响应头带上”。
- 配 CORS 先分清场景:单接口
@CrossOrigin,全局 MVCWebMvcConfigurer,有 Security 必须走http.cors()+ 放行OPTIONS。 - 带凭据时别用
allowedOrigins("*"),换allowedOriginPatterns。 - 别指望 yml 里写几个键就完事——那不会注册任何东西。
来源
- Fix CORS Errors in Spring Boot (3 Ways) — Amigoscode
- Spring Boot CORS Configuration: @CrossOrigin vs WebMvcConfigurer
- How to Fix CORS in Spring Boot with CorsFilter and Spring Security — JavaThinking
- CORS preflight blocked after Spring Boot 4 upgrade — entur/sobek issue
- 从“跨域报错到彻底解决”:Spring Boot+Security+JWT 实战