Spring Boot跨域问题全解析:四种CORS配置方案与Spring Security集成
2026/9/9 9:19:56 网站建设 项目流程

前端同事又在群里喊"接口跨域了",后端拿 Postman 一测,接口通得飞起,两边谁也说不服谁。这种"浏览器报错、工具不报错"的现象,根源不是后端接口挂了,而是浏览器的同源策略把响应拦了下来。本文针对 Spring Boot 项目里的跨域问题,整理了四种从局部到全局、从 MVC 层到 Security 层的解决方案,覆盖绝大部分项目的实际需求。无论你是刚接手前后端分离项目的新手,还是被 Spring Security 跨域配置坑过的人,都能在这里找到对应的解法和完整的排查思路。

1. 跨域问题的本质:浏览器拦的不是请求,是响应

1.1 同源策略到底管什么

同源策略是浏览器最基础的安全机制之一。它规定页面只能读取"同源"的接口响应,所谓同源,是协议、域名、端口三者完全一致。只要有一个不一样,就构成跨域:

  • http://localhost:8080 请求 http://localhost:5173 是跨域(端口不同)
  • http://api.example.com 请求 https://www.example.com 是跨域(协议不同 + 子域不同)
  • http://example.com 请求 http://account.example.com 是跨域(子域不同)

需要注意,这里的"拦截"发生在浏览器这一层。后端接口实际收到了请求,也正常返回了数据,甚至业务逻辑都执行完了,但浏览器发现响应头里没有允许跨域的声明,就直接把响应丢弃了,控制台抛出一串 CORS 报错。这也是为什么很多新人会误以为"跨域是后端问题,接口没通"。

1.2 简单请求与预检请求

CORS(Cross-Origin Resource Sharing)是 W3C 制定的跨域资源共享规范,核心思路是:浏览器在发起跨域请求时,先看服务器返回的响应头是否允许当前来源(Origin)访问。

跨域请求分两类:

  • 简单请求:使用 GET、HEAD、POST 方法,且 Content-Type 仅限于 application/x-www-form-urlencoded、multipart/form-data、text/plain。这类请求会直接发出,浏览器再检查响应头。
  • 预检请求:请求方法为 PUT、DELETE、PATCH,或者 Content-Type 为 application/json,或者带了自定义头,浏览器会先发一个 OPTIONS 请求,询问服务器是否允许。

前后端分离项目里,前端几乎都是 JSON 交互,所以绝大多数请求都会触发预检。这个 OPTIONS 请求让不少人翻过车:后端 Controller 只写了 @PostMapping,没处理 OPTIONS,结果预检请求直接返回 404 或 405,真实请求自然也就发不出去了。

1.3 为什么 Postman 测不出来

Postman 这类接口测试工具不执行浏览器的同源策略,也不关心响应头里的 Access-Control-Allow-Origin,所以接口在 Postman 里永远是"通的"。这就是跨域问题最坑的地方:后端验证没问题,前端就是调不通。理解这一点,你就能明白为什么解决跨域的核心,是让服务器在响应里正确带上 CORS 响应头,而不是去改接口逻辑。

2. 方式一:@CrossOrigin 注解——局部接口的快速解法

2.1 注解的两种使用位置

Spring Boot 从 4.2(Spring 框架版本)开始支持 @CrossOrigin 注解,可以直接加在 Controller 类上,也可以加在方法上。加在类上表示当前 Controller 下所有接口都生效,加在方法上则只作用这一个方法。方法上的配置会覆盖类上的配置。

@RestController @RequestMapping("/api/user") @CrossOrigin(origins = "http://localhost:5173", maxAge = 3600) public class UserController { @GetMapping("/list") public Result list() { // 业务逻辑 return Result.success(); } }

如果需要更细的控制,可以把某个方法单独拎出来配置:

@RestController @RequestMapping("/api/order") public class OrderController { @PostMapping("/create") @CrossOrigin(origins = {"http://localhost:5173", "https://admin.example.com"}, allowedHeaders = "*", methods = {RequestMethod.POST, RequestMethod.OPTIONS}) public Result create(@RequestBody OrderDTO dto) { // 业务逻辑 return Result.success(); } }

2.2 完整参数说明

@CrossOrigin 注解常用属性有这些:

属性作用默认值
origins / value允许的跨域来源列表默认允许所有来源
originPatterns允许的来源通配符模式默认允许所有来源
allowedHeaders允许的请求头默认允许所有请求头
exposedHeaders允许前端读取的响应头空白
methods允许的 HTTP 方法默认允许 Controller 中已映射的方法
allowCredentials是否允许携带 Cookiefalse
maxAge预检请求结果的缓存时间(秒)1800

这里有一个容易被坑的细节:设置 allowCredentials = "true" 之后,origins 就不能写 "" 了。浏览器明确规定,允许携带凭证时,Access-Control-Allow-Origin 必须是明确的源,不能是通配符。一旦你用 "" 又开了 allowCredentials,浏览器会直接报错。如果你确实想放开任意来源,又要带凭证,可以把参数换成 originPatterns = "*",Spring 内部会把它处理成具体的源返回给浏览器。

2.3 注解方式的适用边界

注解方式的优势是快、直观,适合局部接口跨域、临时联调、平台对外开放的接口等场景。但缺点也很明显:

  • 每个 Controller 都要写注解,项目大了代码冗余,还容易漏。
  • 跨域策略分散在各个类里,不好统一管理。
  • 如果一个接口既被后端管理后台调用,又被 C 端小程序调用,你就要在注解里写一堆 origins,维护成本高。

所以我的建议是:注解方式适合做"补充"或者"快速救火",不适合作为项目的全局统一方案。

3. 方式二:WebMvcConfigurer 全局配置——大多数项目的最佳起点

3.1 配置类写法

全局解决跨域问题,最常用的方式是实现 WebMvcConfigurer 接口,重写 addCorsMappings 方法。这种方式不需要碰 Controller 代码,一个配置类统管所有接口。

@Configuration public class CorsConfig implements WebMvcConfigurer { @Override public void addCorsMappings(CorsRegistry registry) { registry.addMapping("/**") // allowedOriginPatterns 支持通配符,且可以和 allowCredentials 共存 .allowedOriginPatterns("*") .allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS") .allowedHeaders("*") .exposedHeaders("Content-Disposition") .allowCredentials(true) .maxAge(3600); } }

这段配置的含义是:对所有路径(/**)生效,允许所有来源跨域,允许 GET、POST、PUT、DELETE、OPTIONS 五种请求方法,允许携带任何请求头,允许携带 Cookie,预检请求结果缓存 3600 秒。

如果你的项目只需要放行特定来源,把 allowedOriginPatterns 改成具体地址即可:

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

3.2 allowedOriginPatterns 和 allowedOrigins 的区别

这一步是很多人写错的。早期资料里常见的是 allowedOrigins(""),但前面说过,allowCredentials(true) 时 allowedOrigins 不允许为 "",启动时不会报错,浏览器访问时却会拦截。allowedOriginPatterns 是 Spring 5.3 开始提供的替代方案,它支持通配符模式(如 https://.example.com),并且在开启 allowCredentials 时也能正常使用。Spring 会把匹配到的具体源放进 Access-Control-Allow-Origin 响应头里,而不是直接返回 "",这就满足了浏览器的限制。

所以现在的惯例是:需要动态放行多个来源时优先用 allowedOriginPatterns,固定单来源时两者都可以,但项目里最好统一一种写法,避免混用出问题。

3.3 全局配置的生效原理

addCorsMappings 配置的对象是 Spring MVC 的处理器映射层。当一个跨域请求进来时,Spring 会在 HandlerMapping 阶段通过 CorsInterceptor 处理预检请求,并给真实响应添加 CORS 响应头。也就是说,只要请求能进到 Spring MVC 的派发流程,这个配置就有效。

由此可以推断出它的局限:如果请求压根没进到 Spring MVC,比如被前置的过滤器(Filter)直接拦截了,比如项目里还挂了 Spring Security,那这个配置就管不到。这也是下一节要讲过滤器方案和第五节要讲 Security 配置的原因。

4. 方式三:CorsFilter 过滤器——脱离 MVC 的更底层方案

4.1 CorsFilter 的标准写法

CorsFilter 是 Spring 提供的 Servlet 过滤器,属于 javax.servlet 规范层面的组件。它作用于整个 Servlet 容器,比 Spring MVC 更底层,因此请求在进入 DispatcherServlet 之前就会加上 CORS 响应头。写法如下:

@Configuration public class CorsFilterConfig { @Bean public CorsFilter corsFilter() { CorsConfiguration config = new CorsConfiguration(); // 允许携带 Cookie config.setAllowCredentials(true); // 与 allowedOriginPatterns 同理,支持通配符且能和 allowCredentials 共存 config.addAllowedOriginPattern("*"); config.addAllowedHeader("*"); config.addAllowedMethod("*"); config.setMaxAge(3600L); // 暴露给前端的响应头,如果需要前端读取特殊头就加上 config.addExposedHeader("Content-Disposition"); UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(); source.registerCorsConfiguration("/**", config); return new CorsFilter(source); } }

和前面的全局配置相比,这段代码的核心对象是 CorsConfiguration 和 UrlBasedCorsConfigurationSource,前者定义策略,后者把策略注册到指定路径模式上。CorsFilter 在过滤器链中执行,所以它的生效范围是整个 Web 应用,包括那些被 Spring MVC 处理不了的请求路径。

4.2 过滤器方案和全局配置方案的取舍

这两种方案都能全局生效,但切入层面不同:

对比项WebMvcConfigurerCorsFilter
底层归属Spring MVC 处理器映射层Servlet 过滤器层
生效时机进入 HandlerMapping 阶段进入 Servlet 容器后、DispatcherServlet 之前
适用范围标准的 Spring MVC 请求整个 Web 应用,包括非 MVC 路径
能否被 Spring Security 影响会被 Security 过滤器链影响取决于过滤器顺序
推荐指数高(常规项目首选)中(有特殊过滤器场景时选择)

实际项目里,如果只是标准的前后端分离,用 WebMvcConfigurer 就足够了;如果项目里存在自定义 Filter 做鉴权、接口签名校验等逻辑,同时这些自定义 Filter 会先于 Controller 处理请求并直接返回响应,那跨域配置就必须放在 Filter 层做,否则请求在自定义 Filter 就被处理掉了,MVC 层的 CORS 配置根本没有机会执行。

4.3 过滤器顺序问题

自己注册 CorsFilter 时要注意过滤器顺序。如果项目里还有别的过滤器,比如 Spring Security 的 DelegatingFilterProxy,CorsFilter 应该放在足够靠前的位置,确保跨域响应头最先加上。

用 FilterRegistrationBean 注册时,可以通过 setOrder 控制顺序:

@Bean public FilterRegistrationBean<CorsFilter> corsFilterRegistration(CorsFilter corsFilter) { FilterRegistrationBean<CorsFilter> registration = new FilterRegistrationBean<>(corsFilter); registration.setOrder(Ordered.HIGHEST_PRECEDENCE); return registration; }

当然,在 Spring Boot 里直接声明 @Bean CorsFilter 通常会被自动注册,顺序一般也够用。这个优先级问题主要出现在你手写 Servlet 过滤器链、或者把应用部署到传统 Servlet 容器时,需要多留个心眼。

5. 方式四:Spring Security 集成时的 CORS 配置——最容易踩坑的场景

5.1 为什么 Spring Security 会让跨域配置失效

很多项目真实情况是:前后端分离 + Spring Security + JWT 认证。这种组合下,如果你只配了 WebMvcConfigurer,跨域可能依然报错。原因是 Spring Security 的过滤器链在整个请求链路中非常靠前,它不认 Spring MVC 的 addCorsMappings 配置。未认证的跨域请求会被 Security 拦截,预检请求也可能在过滤器链上被拒掉,响应头里根本没机会出现 Access-Control-Allow-Origin。

说得直白点:Security 在请求到达 Controller 之前就把门关上了,Spring MVC 的跨域配置在门内,自然用不上。

5.2 在 Security 配置类里开启 CORS

解决办法是让 Security 自己处理 CORS。Spring Security 从 5.x 开始支持 http.cors(),它会去容器里找名为 corsConfigurationSource 的 Bean,拿到配置后由 CorsFilter 完成跨域响应头的写入。

@Configuration @EnableWebSecurity public class SecurityConfig { @Bean public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { http // 启用 CORS,配合下面的 CorsConfigurationSource Bean .cors(cors -> cors.configurationSource(corsConfigurationSource())) // 前后端分离项目关闭 CSRF,具体根据项目决定 .csrf(AbstractHttpConfigurer::disable) .authorizeHttpRequests(authz -> authz .requestMatchers("/api/auth/**").permitAll() .anyRequest().authenticated() ) .addFilterBefore(jwtAuthFilter(), UsernamePasswordAuthenticationFilter.class) .formLogin(form -> form.disable()) .httpBasic(basic -> basic.disable()); return http.build(); } @Bean public CorsConfigurationSource corsConfigurationSource() { CorsConfiguration config = new CorsConfiguration(); config.setAllowCredentials(true); config.addAllowedOriginPattern("*"); config.addAllowedHeader("*"); config.addAllowedMethod("*"); config.setMaxAge(3600L); UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(); source.registerCorsConfiguration("/**", config); return source; } }

这段配置里有几个关键点:

  • .cors() 是启用 Security 对 CORS 的处理,参数里的 configurationSource 指定用哪个配置源。
  • CorsConfigurationSource Bean 的名字必须是 corsConfigurationSource,因为 Security 默认按这个名称查找;如果你想换名字,就得在 .cors(cors -> cors.configurationSource(...)) 里显式指定。
  • 不需要再往 Spring MVC 里配 WebMvcConfigurer,两边的配置会叠加,容易出现响应头重复的情况。

5.3 认证失败、预检请求返回 401 的排查链路

Security 场景下跨域排查,最典型的现象有两个:预检请求返回 401,或者真实请求返回 403 且响应头里没有 CORS 头。遇到这类问题,可按下面的链路一步步排查:

  1. 看浏览器 Network 面板,确认是否有 OPTIONS 预检请求。没有预检请求,说明请求是简单请求或者浏览器没开 CORS。
  2. 如果 OPTIONS 返回 401/403,先确认 Security 是否已经把/preflight 相关的 URL 放行。预检请求通常不带认证信息,如果被 Security 当作未认证请求拦截,就回不到业务层了。必要时为 OPTIONS 请求放行:
.requestMatchers(HttpMethod.OPTIONS, "/**").permitAll()

这一步属于临时手段,真正稳妥的还是靠 .cors() 让 Security 在过滤器链上先把预检请求处理掉,而不是交到认证逻辑里去。

  1. 看响应头。如果真实请求的响应里有 Access-Control-Allow-Origin,但浏览器还是报错,多半是 allowCredentials 与 allowedOrigin 匹配的问题,或者暴露头不足。
  2. 后端日志看不出毛病,那就用 curl 手动带上 Origin 头复现:
curl -i -X OPTIONS http://localhost:8080/api/user/list \ -H "Origin: http://localhost:5173" \ -H "Access-Control-Request-Method: GET"

手动构造预检请求,能直接看到服务器返回了哪些 CORS 响应头,比在浏览器里猜快得多。

6. 四种方式对比与真实项目选型建议

6.1 对比总览

方案代码位置作用范围是否依赖 Spring MVC适合场景
@CrossOrigin 注解Controller 类或方法局部接口快速救火、个别接口对外开放
WebMvcConfigurer 全局配置配置类全部 Controller普通前后端分离项目首选
CorsFilter配置类 + Bean全部 Web 请求自定义 Filter 多、非标准 MVC 场景
Spring Security CORSSecurityConfigSecurity 过滤链上的请求整合了 Security/JWT 的项目

四种方式不是互斥的,但我不建议在同一个项目里混用。比较常见的情况是:全局配置已经处理了大部分接口,某个管理员专用的 Controller 又单独加了 @CrossOrigin,结果响应头里出现两个 Access-Control-Allow-Origin,浏览器直接报"该响应头包含多个值"之类的错误。既然有全局方案,局部注解能不加就不加。

6.2 基于真实项目的选型思路

我自己的经验是:

  • 新项目如果没接 Spring Security,直接用 WebMvcConfigurer 方案,代码最少,维护最简单。
  • 项目已经接了 Spring Security,直接把 CORS 配置放到 SecurityConfig 里,并且删掉 WebMvcConfigurer 那套,避免两头配置打架。
  • 如果有比较重的前置过滤器(如网关层面的接口验签),优先用 CorsFilter,并在过滤器链里把 CorsFilter 排在最前面。
  • 至于 @CrossOrigin,我只会在两个场景用它:一是给外部门户单独提供开放接口,二是排查问题时临时验证某个接口是否真的跨域。

6.3 还是调不通?从浏览器和部署层继续排查

如果四种方式都配过了,前端还是报跨域,问题往往不在代码上。几个高频的"假跨域"原因:

  • Nginx 反向代理配置不对。应用部署到服务器后,前端访问的是 Nginx 的域名和端口,后端服务跑在内部端口,如果 Nginx 没有把跨域响应头转出去,前端拿到的响应自然没有 CORS 头。这时候去 Nginx 的 location 配置里加 add_header 相关设置,或者修改后端 CORS 来源为前端实际访问的域名。
  • 浏览器插件或代理工具篡改了请求头,导致后端 CORS 配置匹配不上 Origin。
  • 前端没走代理配置。本地开发时,很多人会用 Vite 或 Webpack 的 devServer 代理转发请求,转发后同源就消除了跨域。如果 devServer 配置没生效,请求还是会直接发到后端,这时候后端配置必须有对应的 Origin。两种情况混着来,问题就容易反复。

其中一个我反复踩过的点就是"本地通了、上线又断"。本地开发靠前端代理转发,同源所以没跨域,一上测试环境走了 Nginx直连后端,跨域又冒出来。

6.4 最后分享一个小技巧

配置里一定要把 maxAge 设上,比如 3600 秒。它让浏览器在缓存有效期内不再发起预检请求,每次真实请求直接发送,既省流量又降低接口延迟。尤其在毫秒级接口上,每次多一次 OPTIONS 往返,用户体感是能明显感到变慢的。

跨域问题本质上不是"把跨域禁掉",而是让浏览器确信这个跨域来源是被服务器允许的。搞懂了这个机制,再去配置那四种方式,心里就有底了,后续遇到任何奇怪的 CORS 报错,也能顺着响应头和请求链路一步步定位,而不是病急乱投医。

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

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

立即咨询