Spring Boot CORS跨域配置与排错:前后端分离联调指南
2026/9/19 15:04:14 网站建设 项目流程

简介:Spring Boot 开发者常遇到的跨域问题,在这份 PDF 文档中得到系统梳理,资源面向 Java Web 开发者和前后端分离项目维护人员,讲解 CORS 跨域资源共享机制及其在 Spring Boot 中的落地。文档按两条主线展开:一是自定义 CorsFilter,重写 OncePerRequestFilter 的 doFilterInternal 方法,在响应头中配置 Access-Control-Allow-Origin、Access-Control-Allow-Credentials、Access-Control-Allow-Methods、Access-Control-Max-Age 和 Access-Control-Allow-Headers,并处理 OPTIONS 预检请求;二是通过 @Configuration 定义 CorsConfig,结合 UrlBasedCorsConfigurationSource、CorsConfiguration 与 FilterRegistrationBean 完成全局配置,可通过 addAllowedOrigin、addAllowedHeader、addAllowedMethod 精确控制允许的域名、请求头和请求方法,同时用 setOrder 控制过滤器优先级。PDF 内包含可直接参考的 Filter 实现和配置类代码片段,也结合跨域概念解释配置背后的原理,并点明两种方式各自的适用场景。包体为 1 个 PDF 文件,压缩包约 34KB,轻量易读,目前已有 2834 人浏览学习。文档还总结了两种方式的优缺点和选型建议,适合需要快速排查前后端联调跨域报错、或想在不同项目中灵活选择配置方式的 Java 工程师。

1. springboot cors 跨域报错是前后端分离最常见的联调问题

springboot cors 跨域报错是前后端分离项目最常见的联调问题:页面在 5173 端口,接口在 8080 端口,前端一个 fetch 就抛 has been blocked by cors policy: no 'access-control-allow-origin' header is present。这个报错和 Spring Boot 本身没关系:请求实际到了后端,是浏览器读到响应没有 Access-Control-Allow-Origin 头才拒绝放行。springboot 配 cors 的实质是让 MVC 按规则给响应补头、按规范处理 OPTIONS 预检,最常见的就是全局配置和 @CrossOrigin 注解两种方式。文章按浏览器检查顺序讲参数、allowedOriginPatterns 与 credentials 的坑,以及拦截器和 Security 如何破坏配置,适合联调被跨域卡住的人,也够得着 springboot 面试里 cors 配置错误这道题。

2. 预检机制与 6 个响应头:配 springboot cors 前先看清浏览器要什么

2.1 简单请求与预检请求的分界

浏览器不是对所有跨域请求都先发 OPTIONS。满足全部条件的叫简单请求:方法落在 GET/HEAD/POST,Content-Type 只能是 application/x-www-form-urlencoded、multipart/form-data、text/plain,且没有 authorization 之类的自定义头。满足时浏览器直接发真实请求,后端只要在响应里带 Access-Control-Allow-Origin 就能通过。

条件任一不满足——接口要求 application/json、调用方带了 Authorization 头、用了 PUT/DELETE——浏览器就先发 OPTIONS 预检,请求头里带 Access-Control-Request-Method 和 Access-Control-Request-Headers,问"我打算这么调,你让不让"。后端匹配并返回一组 Access-Control-Allow-* 头,预检算通过;不匹配或没配 cors,浏览器直接拦掉真实请求,console 里就是那段 blocked 文案。所以排查第一步永远是打开 Network 看有没有 OPTIONS 记录:有,是预检环节挂了;没有,是真实响应缺头。

2.2 Access-Control-* 响应头参数与手动预检命令

六个响应头决定预检和真实请求是否通过,它们在 Spring 里的配置入口如下表:

响应头配置入口作用翻车点
Access-Control-Allow-OriginallowedOrigins / allowedOriginPatterns声明允许哪个来源读响应配了*又开 credentials
Access-Control-Allow-MethodsallowedMethods预检放行的 HTTP 方法漏掉 PUT/DELETE
Access-Control-Allow-HeadersallowedHeaders预检放行的自定义请求头漏掉 authorization
Access-Control-Allow-CredentialsallowCredentials是否允许带 cookie 凭据*冲突
Access-Control-Expose-HeadersexposedHeaders前端 JS 能读到的响应头白名单漏掉 X-Total-Count
Access-Control-Max-AgemaxAge预检结果在浏览器缓存秒数设太大导致改配置不生效
# 在本地复现浏览器预检,路径换成实际接口 curl -i -X OPTIONS 'http://localhost:8080/api/v1/order/1' \ -H 'Origin: http://localhost:5173' \ -H 'Access-Control-Request-Method: GET' \ -H 'Access-Control-Request-Headers: authorization'

命令说明:-i打印响应头,三组-H是浏览器发预检时原样带上的内容;后端返回的 Access-Control-Allow-Origin 等于http://localhost:5173,说明 MVC 的 cors 开关已开。参数说明:Origin 必须和页面实际域名端口完全一致;带不带 authorization 那句决定 Access-Control-Request-Headers 是否出现,业务里用了 token 就一定会有。

2.3 Spring 家族里 cors 生效的两层位置

第一层在 DispatcherServlet 内部,由 AbstractHandlerMapping 负责。它按路径找到 HandlerExecutionChain 后,把合并好的 CorsConfiguration 包装成 CorsInterceptor 塞进执行链;预检请求在这一步由 DefaultCorsProcessor 直接写响应,不会进入 Controller。addCorsMappings 写的全局配置和 @CrossOrigin 注解都注入到这一层。

第二层是 CorsFilter,一个普通 Servlet Filter,跑在 DispatcherServlet 之前,不依赖路径匹配,Spring Security 和从旧 springmvc 工程改造过来的 Filter 链里都能用。Spring Boot 自动配置原理里有对应的 CorsAutoConfiguration:项目没定义 CorsFilter Bean、但写了 spring.web.cors.* 属性时,它会自动装配一个 CorsFilter。大多数人不会走属性文件这条路,因为写不了复杂规则,但对"springmvc 工程如何改造成 springboot 工程"的老项目,这个自动装配常常是重复响应头的来源。

提示:同一个请求可能被两层同时处理。Filter 补一次头,HandlerMapping 再补一次头,浏览器看到重复的 Access-Control-Allow-Origin 直接拒绝,这是配完还报 blocked 的隐藏原因。

3. 方式一:addCorsMappings 全局配置 springboot cors 与 allowedOriginPatterns

3.1 最小全局配置与链式参数表

@Configuration public class CorsConfig implements WebMvcConfigurer { @Override public void addCorsMappings(CorsRegistry registry) { registry.addMapping("/api/**") .allowedOrigins("http://localhost:5173", "https://admin.example.com") .allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS") .allowedHeaders("*") .exposedHeaders("X-Token", "Content-Disposition") .allowCredentials(true) .maxAge(3600); } }

代码逻辑说明:实现 WebMvcConfigurer 的 @Configuration 会在启动时被回调,addCorsMappings 把 CorsRegistration 注册到 RequestMappingHandlerMapping 的全局配置里,所有匹配路径的接口共用一份规则。addMapping 是路径匹配,/api/**只覆盖业务接口;/**最省事但后面挂 Security 时范围越宽越难管。

链式参数对应行为默认值说明
allowedOriginsAccess-Control-Allow-Origin白名单 Origin;与 allowCredentials(true) 并存时禁止*
allowedOriginPatternsAccess-Control-Allow-Origin通配模式,匹配后回显请求方 Origin
allowedMethodsAccess-Control-Allow-MethodsGET/HEAD/POST漏配 PUT/DELETE 时预检直接失败
allowedHeadersAccess-Control-Allow-Headers默认不限制显式配置后才按白名单校验
exposedHeadersAccess-Control-Expose-Headers不配的话前端 getResponseHeader 拿不到值
allowCredentialsAccess-Control-Allow-Credentialsfalsetrue 表示允许携带 cookie 与 Authorization
maxAgeAccess-Control-Max-Age1800 秒浏览器缓存预检结果的时间

allowedMethods 默认只有 GET/HEAD/POST 是新手最容易踩的点:接口用 PUT 更新,前端请求是发出去了,但预检先失败,Network 里始终只有一条 OPTIONS,控制台却不解释为什么。

3.2 allowedOrigins 与 allowedOriginPatterns:credentials=true 时的坑

前端 axios 一旦withCredentials: true,或后端接口要读 cookie,Access-Control-Allow-Origin 就不能是*,必须是具体 Origin。老写法:

.allowedOrigins("*") .allowCredentials(true)

在 Spring Framework 5.3 之前能启动但浏览器按规范拒绝;5.3 引入 allowedOriginPatterns 后推荐替代方案;到 Spring Boot 3 这一代,直接这样写会在启动阶段抛 IllegalArgumentException,报错信息就是这个场景的经典文案"cors 配置错误(反射 origin + credentials=true)"。这也是项目从 Boot 2.3 升到 Boot 3 后突然启动失败的高频原因,跟 springboot 版本太高、行为收紧直接相关。

正确做法是窄化来源,或者用模式匹配:

registry.addMapping("/**") .allowedOriginPatterns("http://localhost:*", "https://*.example.com") .allowedMethods("*") .allowCredentials(true);

参数说明:allowedOriginPatterns 不是把*原样塞进响应头,而是后端拿请求 Origin 与模式比对,匹配后把实际 Origin 回显到 Access-Control-Allow-Origin。localhost:*能覆盖前端经常变的随机端口,*.example.com覆盖多级子域。安全审计严格的话还是建议显式 allowedOrigins 白名单,模式匹配是便利和安全的折中。

3.3 全局方案的另一个形态:注册 CorsFilter Bean

部分场景不适合走 MVC 层:接口路径不归 @RequestMapping 管、想在 Filter 链最前面处理、或者要跟 Spring Security 共用同一个配置源。常见做法是直接注册 CorsFilter:

@Bean public CorsFilter corsFilter() { CorsConfiguration config = new CorsConfiguration(); config.setAllowedOriginPatterns(List.of("http://localhost:*")); config.setAllowedMethods(List.of("*")); config.setAllowCredentials(true); UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(); source.registerCorsConfiguration("/**", config); return new CorsFilter(source); }

逻辑说明:CorsFilter 构造器依赖一个 CorsConfigurationSource,UrlBasedCorsConfigurationSource 负责按路径返回配置;registerCorsConfiguration 可以写多行做按模块的差异化放行。它与 addCorsMappings 的区别在于执行层级:一个在 Servlet Filter,一个在 MVC HandlerMapping,两者同时存在就会产生重复头。我的建议是:纯 Spring MVC 项目用 addCorsMappings;涉及 Spring Security 时把配置抽成 CorsConfigurationSource 的 @Bean 给两边共用,避免维护两套。

4. 方式二:用 @CrossOrigin 注解细粒度放行 springboot 接口

4.1 类级与方法级的 @CrossOrigin 写法

@RestController @RequestMapping("/api/open") @CrossOrigin(origins = "http://localhost:5173", maxAge = 3600) public class OpenApiController { @GetMapping("/health") public String health() { return "ok"; } @GetMapping("/orders") @CrossOrigin( origins = {"https://admin.example.com", "https://ops.example.com"}, allowedHeaders = {"authorization", "content-type"}, exposedHeaders = "X-Total-Count", allowCredentials = "true" ) public List<Order> list() { return orderService.list(); } }

代码逻辑说明:类级注解对 Controller 下所有方法生效;方法级注解与类级注解做合并,同名属性以方法为准,方法没写的属性沿用类级值。所以上面的 health 接口只放行 localhost:5173,orders 接口额外放开两个线上来源,且允许前端读取 X-Total-Count 响应头。

@CrossOrigin 的完整属性表:

属性类型默认值映射目标
origins(别名 value)String[]{}Access-Control-Allow-Origin
allowedHeadersString[]{}Access-Control-Allow-Headers
methodsRequestMethod[]{}为空时取 Controller 映射方法本身
exposedHeadersString[]{}Access-Control-Expose-Headers
allowCredentialsString""注意是字符串"true"/"false",不是布尔值
maxAgelong-1预检缓存秒数,-1 表示不产生该头

新手最容易写错的是 allowCredentials 传了布尔 true:@CrossOrigin 的属性类型是 String,传 boolean 会直接编译报错。

4.2 细粒度场景:第三方来源、自定义请求头与暴露头

@CrossOrigin 适合"系统里只有几个接口对外开放"的局面,比如健康检查、支付回调、开放平台 API。第三方来源往往不是一个域,origins 数组直接列多个。前端要读 X-Total-Count 做分页,后端必须 exposedHeaders 声明,否则 getResponseHeader 返回 null;前端请求头带了自定义 header,allowedHeaders 得包含它,否则预检阶段就被否决。注解方式的优点是配置跟着接口走,一个接口一套规则,代码评审时看 Controller 就清楚谁对谁开放了跨域,不需要再全局翻配置类。

4.3 注解方案的边界:这些场景 @CrossOrigin 不生效

注解只在请求到达 Spring MVC 的 HandlerMapping 之后才有意义。请求在 DispatcherServlet 之前被 Spring Security、网关或前置 Filter 拦截时,注解配置根本没机会作用;判断方法是看 Network 里失败响应有没有 Access-Control-Allow-Origin 头,有说明 MVC 层处理了,没有就往上找拦截层。路径没匹配到 Controller 时同理:404 响应不会携带注解生成的跨域头,前端报错前先确认 URL 前缀没拼错。

另一个容易混的是权限语义:@CrossOrigin 只决定浏览器是否放行响应,不代表接口匿名可访问。认证逻辑照常执行,加了注解不等于免登录。实际项目里"加了 @CrossOrigin 还是 401"大多是这个问题,和后端 cors 配置本身没关系。

5. springboot cors 配完还报 blocked 的 4 个排查点与 curl 验证

5.1 排查点一:Spring Security 把 OPTIONS 预检挡在门外

@Configuration public class SecurityConfig { @Bean SecurityFilterChain filterChain(HttpSecurity http) throws Exception { http.cors(Customizer.withDefaults()) .authorizeHttpRequests(auth -> auth .requestMatchers(HttpMethod.OPTIONS, "/**").permitAll() .anyRequest().authenticated()); return http.build(); } }

代码说明:http.cors()会从容器里找 CorsConfigurationSource Bean,没有就用默认实现;OPTIONS 先放行,否则预检请求会被认证逻辑拦截,返回 401,响应里自然没有跨域头。Spring Boot 2 的写法是antMatchers(HttpMethod.OPTIONS, "/**").permitAll(),原理相同。同时确认你在 Security 里用了同一个 CorsConfigurationSource,别让 Security 读一套、MVC 用另一套。

5.2 排查点二:404、自定义拦截器与重复头

preflight OPTIONS 请求如果路径不匹配任何 handler,HandlerMapping 直接返回 404,响应头里不会有 Access-Control-Allow-Origin,浏览器判定预检失败——这跟配置写没写对无关。前端 baseURL 多拼一段、后端 context-path 不一致都会触发。自定义拦截器是第二个坑:很多 springboot 拦截器实现里校验登录态,对没有 token 的 OPTIONS 直接 return false,预检要求 2xx 响应,401/403 照样被浏览器拦截。常见做法是在 preHandle 第一行放行 OPTIONS 请求,再把业务校验放在后面。

第三个常见失败是重复的 Access-Control-Allow-Origin 头。注解和 addCorsMappings 同时生效会叠加;nginx、网关层手动 add_header 之后后端 Filter 又补一次,也会出现两行同名头,浏览器的报错文案是"multiple values"。用 curl 看响应头,两行一样的值就是证据。

5.3 用 curl 和 Vary 头验证 cors 配置是否真正生效

# 验证真实请求的响应头 curl -s -D - -o /dev/null 'http://localhost:8080/api/v1/order/1' \ -H 'Origin: http://localhost:5173' # 验证预检请求,前端最常见的失败环节 curl -s -D - -o /dev/null -X OPTIONS 'http://localhost:8080/api/v1/order/1' \ -H 'Origin: http://localhost:5173' \ -H 'Access-Control-Request-Method: GET' \ -H 'Access-Control-Request-Headers: authorization'

命令说明:-D -把响应头打印到标准输出,-o /dev/null丢弃 body。看完输出只判断三件事:Access-Control-Allow-Origin 的值是否和请求 Origin 完全一致(含端口);Vary头里是否带 Origin,这是缓存区分来源的关键标志;credentials 场景下 Allow-Origin 不允许是*。三条都过,配置本身没问题,剩下的怀疑对象就是浏览器缓存和上层代理;任意一条不过,直接用返回的状态码和缺失头去定位是哪一层没放行。

浏览器里改完配置还报旧错时,开发期把 maxAge 临时设成 0,让每次请求都重新预检,联调通过后再提到 600 秒收尾——用这招能过滤掉一半"我明明改对了"的自我怀疑。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询