☰
Spring Boot CORS跨域配置:全局addCorsMappings与@CrossOrigin注解实战
2026/10/29 10:59:01 网站建设 项目流程

简介:Spring Boot 中处理 CORS 跨域请求的两种主流实现方式,适合后端开发者在前后端分离或接口调试场景下参考。资源以 PDF 文档形式,完整对比了基于 CorsFilter 的过滤器方案与基于 CorsConfig 的 Java 配置方案,包含具体代码片段、请求头设置说明以及 Filter 注册与顺序控制要点,可直接用于项目改造和排错。压缩包共 1 个 PDF 文件,整体大小约 34KB,轻量易读,适合快速查阅。已有 2835 人学习,内容覆盖 CorsFilter 重写 doFilterInternal、允许跨域携带凭证、配置允许域名与请求方法等细节,同时对 CORS 机制原理和方案选用建议做了补充,能够帮助读者快速理解并落地跨域配置。

1. CORS 跨域请求的两种配置方式:一个注解省事,一个全局兜底,难点全在响应头

前后端分离的项目里,跨域报错几乎是每个开发者的第一道坎。浏览器控制台里出现 “Access to XMLHttpRequest at 'http://localhost:8081/api/users' from origin 'http://localhost:8080' has been blocked by CORS policy”,后端接口明明已经返回数据了,前端就是拿不到。Spring Boot 解决这个问题有两条官方路线:实现 WebMvcConfigurer 重写 addCorsMappings 做全局配置,或者在 Controller 类和方法上用 @CrossOrigin 注解做细粒度控制。前者适合整个项目统一放行,后者适合只对特定接口开跨域。

这两条路本身不难,难的是搞清楚浏览器到底拦了什么、Spring Boot 每个参数背后对应哪个响应头。新手经常把配置抄上去发现还是报错,老手也会在 allowedOrigins 和 allowCredentials 的组合上翻车。这篇就顺着“原理—实现—踩坑—验证”把两条路讲透,配置能直接抄,坑也是我实际排过的。

2. CORS 排错先看浏览器而不是服务端:预检请求与四个响应头的机理

跨域问题最迷惑人的点是:服务器从头到尾都没报过错,接口逻辑也正常执行了,前端就是拿不到响应。要理解为什么,得先搞清楚浏览器的同源策略和 CORS 工作机制。

2.1 同源策略:浏览器在“拦截”什么

同源指的是协议、域名、端口三个都相同。http://localhost:8080 和 http://localhost:8081 端口不同,算跨域;https://admin.example.com 去调 https://api.example.com,域名不同,也算跨域。浏览器默认只允许页面去请求同源的资源,这是为了防住恶意站点拿着你的登录态去别的网站偷数据。

CORS 全称是跨域资源共享,它是服务器通过响应头主动声明“我允许某个源来读我的数据”的一套协议。注意这里的关键点:请求其实已经打到了服务器,服务器也正常处理并返回了结果,浏览器在把响应交给前端 JavaScript 之前检查响应头。如果响应头里没有 Access-Control-Allow-Origin,或者值跟当前页面的源对不上,浏览器就直接把响应丢掉,并在控制台报 CORS policy 错误。所以在服务端日志里找跨域报错是找不到的,这是个黑匣子,你得从浏览器 Network 面板和响应头入手。

2.2 简单请求与预检请求:为什么一次请求变成了两条

浏览器把跨域请求分成两类。简单请求的条件比较苛刻:方法限定 GET、HEAD、POST,Content-Type 只能是 application/x-www-form-urlencoded、multipart/form-data、text/plain 之一,而且不能用自定义请求头。简单请求可以直接发出去,浏览器看完响应头再决定要不要放行。

一旦你不满足上面的条件,比如用 PUT 或 DELETE 方法、Content-Type 是 application/json、带了 Authorization 或者 X-Requested-With 这类自定义头,浏览器会先发一个 OPTIONS 预检请求。这个请求不带业务数据,只带两个字段:Access-Control-Request-Method 告诉服务器“接下来我要用啥方法”,Access-Control-Request-Headers 告诉服务器“接下来我会带哪些自定义头”。服务器必须响应正确的 CORS 头,浏览器确认允许后,才发起真正的业务请求。

这就是为什么前端一个 POST 请求在 Network 面板里能看到两条记录:一条 OPTIONS 状态码通常是 204 或 200,一条才是真正的 POST。后端如果不处理 OPTIONS,或者响应头里漏配了方法、请求头,预检就失败,真正的请求根本不会发出。

2.3 四个响应头与一个安全边界:排错要背的表

服务器针对预检和实际请求要返回的核心响应头,排错时对照这张表看就够了:

响应头作用常见坑
Access-Control-Allow-Origin允许哪些源读取响应,必须与请求源精确匹配配了*又开 credentials 直接报错
Access-Control-Allow-Methods预检通过后允许哪些 HTTP 方法只配了 GET、POST,漏了 PUT、DELETE
Access-Control-Allow-Headers允许哪些自定义请求头没配 Authorization,带 token 的请求被拦
Access-Control-Max-Age预检结果可以缓存多少秒不配的话每次跨域请求都先 OPTIONS,拖慢速度

还有一个容易一起出现的安全边界:Access-Control-Allow-Credentials。它表示允不允许浏览器携带 Cookie 跨域发送。一旦这个头设为 true,Access-Control-Allow-Origin 就不能是*,必须是白名单里的具体源。因为*表示“任何源都能拿”,再允许携带 Cookie 会直接把用户的登录态暴露给任意站点,这是 CORS 规范的红线。生产环境几乎都要求带登录态,所以通配符这条路从根上就行不通,这也是下面两种配置方式里最常踩的区别点。

3. 方式一:实现 WebMvcConfigurer 全局放行,多接口统一配置的最佳落点

如果项目里大多数接口都要允许跨域,比如前后端完全分离,全局配置是最省事的。

3.1 最小可用的 addCorsMappings 配置

在任意被 Spring 扫描到的地方加一个配置类,实现 WebMvcConfigurer 接口,重写 addCorsMappings 方法即可:

import org.springframework.context.annotation.Configuration; import org.springframework.web.servlet.config.annotation.CorsRegistry; import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; @Configuration public class CorsConfig implements WebMvcConfigurer { @Override public void addCorsMappings(CorsRegistry registry) { registry.addMapping("/api/**") .allowedOriginPatterns("http://localhost:8080", "https://admin.example.com") .allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS") .allowedHeaders("*") .allowCredentials(true) .maxAge(3600); } }

这段代码是关键入口。@Configuration让 Spring Boot 启动时加载这个类;addMapping("/api/**")限定只有 /api 前缀的接口走 CORS 规则,其他路径不涉及跨域,这是比直接写/**更稳的做法。allowedOriginPatterns里放的就是允许访问的前端页面源,注意要带协议、域名、端口,一个都不能少。allowedMethods里必须带上 OPTIONS,因为预检请求本身就是 OPTIONS 方法,不放在白名单里会被拒。allowCredentials(true)表示允许携带 Cookie,maxAge(3600)表示预检结果缓存 3600 秒,单位是秒。

Spring Boot 3.x 里这段配置写法完全不变,接口类包路径依然是 org.springframework.web.servlet.config.annotation,受影响的是 jakarta 命名空间,跟 CORS 无关。如果你的项目是 Boot 2.4 之前的版本,allowedOriginPatterns这个方法还不存在,要用allowedOrigins,区别在下一小节讲。

3.2 allowedOrigins 还是 allowedOriginPatterns:Spring Boot 2.4 的分水岭

Spring Boot 2.4 之前,全局配置只有allowedOrigins这一个方法。它支持精确的源,也支持一个特殊值*,但*和allowCredentials(true)是互斥的。这在前后端分离项目里很尴尬:要带 Cookie 就必须把源一个个写死,一旦上线环境多了或者源是动态的,配置就变得很僵硬。

2.4 版本开始引入了allowedOriginPatterns,它支持模式匹配,比如写成https://*.example.com就能匹配所有子域。更关键的是,它跟allowCredentials(true)可以共存。实现原理是:用 allowedOriginPatterns 时,Spring 会把浏览器实际请求的 Origin 回显到 Access-Control-Allow-Origin 响应头上,而不是直接返回*,这样就满足了“Allow-Origin 不能为 *,但每个源都能被精确匹配”的要求。

两个方法的核心差异整理成表:

方法支持通配符可与 allowCredentials 共存适用场景
allowedOrigins仅*,不能按子域通配*时不允许2.4 之前的老项目,源固定不变
allowedOriginPatterns支持https://*.example.com支持2.4+ 推荐,生产环境白名单首选

如果项目里用的是高版本 Spring Boot,配置里还写着allowedOrigins("*")而且又开了 credentials,那不是版本太高的兼容问题,而是配置本身违反了 CORS 的硬性规范,换任何版本都会报错。

3.3 全局配置管不到的地方:CorsFilter 与过滤器链顺序

WebMvcConfigurer 的 addCorsMappings 最终是通过 Spring MVC 的 HandlerMapping 机制生效的,它会生成一个 CorsInterceptor,在请求进入 Controller 之前对响应头做处理。但有一个场景它管不到:如果项目里有过滤器在 Spring MVC 之前就写入了响应,比如自定义的鉴权 Filter、接口签名校验 Filter,或者响应直接被网关层代理转发,CORS 头就可能加不上或者被覆盖。

这种时候常见做法是直接用 CorsFilter,它是一个标准的 Servlet Filter,不依赖 Spring MVC,可以手动控制注册顺序:

import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.web.cors.CorsConfiguration; import org.springframework.web.cors.UrlBasedCorsConfigurationSource; import org.springframework.web.filter.CorsFilter; import java.util.Arrays; @Configuration public class CorsFilterConfig { @Bean public CorsFilter corsFilter() { CorsConfiguration config = new CorsConfiguration(); config.setAllowedOriginPatterns(Arrays.asList("http://localhost:8080")); config.setAllowedMethods(Arrays.asList("GET", "POST", "PUT", "DELETE", "OPTIONS")); config.setAllowedHeaders(Arrays.asList("*")); config.setAllowCredentials(true); config.setMaxAge(3600L); UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(); source.registerCorsConfiguration("/**", config); return new CorsFilter(source); } }

CorsFilter 的配置内容和 addCorsMappings 几乎一一对应,只是走的是 Servlet 过滤器链。合作的场景里,如果项目还接了 Spring Security,要让 CorsFilter 排在 SecurityFilterChain 之前,否则 Security 先拦了请求,CorsFilter 根本没机会加头。这就是为什么有些项目明明全局配置写了,登录接口还是跨域报错。

4. 方式二:@CrossOrigin 注解按接口放行,优先级与参数边界要分清

全局配置适合“大部分接口都要开”的情况。如果只是某个特定接口需要跨域,比如开放给第三方调用的查询接口,用 @CrossOrigin 注解更干净,不会影响整个项目的 CORS 策略。

4.1 加在类上还是方法上:优先级怎么算

@CrossOrigin 可以放在类上,也可以放在方法上,还可以两个都放:

import org.springframework.web.bind.annotation.*; @RestController @RequestMapping("/api/users") @CrossOrigin(origins = "http://localhost:8080", maxAge = 3600) public class UserController { @GetMapping("/{id}") @CrossOrigin(origins = "https://admin.example.com") public User getUser(@PathVariable Long id) { return userService.findById(id); } @PostMapping public User create(@RequestBody User user) { return userService.create(user); } }

这段代码里,getUser 方法使用了方法上的 @CrossOrigin,允许的源是 https://admin.example.com;create 方法上没有注解,就继承类上的配置,允许 http://localhost:8080。Spring MVC 处理 CORS 时的优先级是:方法级注解 > 类级注解 > 全局 addCorsMappings 配置。方法上没有注解时才看类上的,类上也没有才落到全局配置。

一个容易误解的点是类上的 @CrossOrigin 只对当前 Controller 类生效,不会波及同包下其他 Controller。想让多个 Controller 共享一份注解策略,只能在每个类上重复标注,或者干脆回到全局配置方式。

4.2 注解参数说明:maxAge、allowCredentials、exposedHeaders 的边界

@CrossOrigin 注解的参数和全局配置基本对应,但有几个默认值需要注意:

参数默认值说明
origins*允许的源,与 allowCredentials 互斥
originPatterns空2.4+ 新增,支持通配模式
allowedMethods与当前方法的 @RequestMapping 方法一致推荐显式写全
allowedHeaders*预检请求允许携带的请求头
exposedHeaders空允许前端 JS 读取的响应头
allowCredentials容器默认置为 true 时允许携带 Cookie
maxAge1800 秒预检结果缓存时长

最容易漏的是 exposedHeaders。后端如果返回了自定义响应头,比如 X-Total-Count 用来告诉前端分页总数,浏览器默认不允许前端 JS 读取非 CORS 白名单里的响应头。这时候需要在注解里显式声明:

@CrossOrigin(origins = "http://localhost:8080", exposedHeaders = "X-Total-Count")

不配的话,前端能收到响应但读取不到这个头,排查时看起来像是接口“返回了却少了数据”,其实是被浏览器挡在了读取这层。这是注解方式特有的一个暗坑,全局配置里同样有对应的 exposedHeaders 方法,实操中经常被忽略。

4.3 注解与全局配置共存时:是合并还是覆盖

有这样一个问题:方法上的 @CrossOrigin 只写了 origins 和 exposedHeaders,那 allowedMethods、maxAge 这些参数是从哪里来的?很多人以为会继承全局配置或者类上的配置,现实是 Spring 在合并 CorsConfiguration 时遵循“就近完整覆盖”的逻辑——一旦找到了最优先级的来源,就整体使用这个来源的配置,缺失的属性走它自己的默认值,而不是去下一级补全。

这就导致一种情况:方法上写了 @CrossOrigin(origins = "http://localhost:8080"),没写 allowedMethods,此时方法的跨域允许方法会退化成默认的 GET、HEAD、POST,而不是类上配的 PUT、DELETE。前端发 PUT 请求时预检直接失败,看起来像“注解配置没生效”。

所以用注解方式时,建议把常用的属性写全,至少 methods、maxAge、allowCredentials 这三维要显式表达。方法的注解策略和全局配置的关系不是“叠加”,而是“谁近谁说了算”,这一点先想清楚,能避免很多玄学问题。

5. 跨域配置的 5 个高频坑:从 blocked by CORS policy 到 Nginx 重复头

这一章全是排障记录。每一条都是实际项目里出现过、并且让前后端联调卡了至少半天的问题,按“现象→原因→解决”写清楚。

5.1 现象:后端日志一片正常,浏览器报 blocked by CORS policy

后端接口执行了,日志里也打印了业务结果,浏览器就是报跨域错误。原因在前面讲过,CORS 是浏览器检查响应头之后做的决定,服务端感知不到。解决的第一步永远是打开浏览器 Network 面板,看那条失败的请求的响应头:有没有 Access-Control-Allow-Origin,值是不是跟前端页面的源完全一致。最常见的问题是源只差了端口,比如页面是 http://localhost:8080,配置写成了 http://localhost:8080/,多了一个斜杠都不算精确匹配。

还有一种情况容易被误导:浏览器报的错是 “The request client is not a secure context”。这是浏览器安全策略层面的限制,不是说你的 CORS 配错了。当开发机用 IP 加 http 访问页面时,部分浏览器会把页面视为非安全上下文,对跨域请求的约束会变得更严格。解决方法是本地开发统一用 http://localhost 访问,生产环境上 HTTPS,别在后端配置里硬调接口,调了也没用。

5.2 现象:allowCredentials(true) 配上 allowedOrigins("*"),启动不报错运行报错

启动项目时一切正常,浏览器里一请求就报 CORS 错误。原因很简单,配置本身违反了 CORS 规范:Access-Control-Allow-Credentials 为 true 时,Access-Control-Allow-Origin 不能是*。浏览器看到这两个头同时存在会直接拒绝。解决方法是把*换成白名单,或者业务不需要 Cookie 时把 allowCredentials 关掉。还要注意前端如果用了 axios,withCredentials 默认是 false,后端开了 credentials 前端也要开,两边不一致时 Cookie 不会带上,表现为登录接口通但 session 一直建立不起来。

5.3 现象:Spring Security 把 OPTIONS 预检请求拦成 403

项目里集成 Spring Security 后,跨域请求的 OPTIONS 预检直接 403,POST 请求反而正常偶尔。原因是 Security 默认拦截所有请求,预检请求只是试探,不带业务凭证,被安全链挡住。解决思路是在安全配置里显式放行 OPTIONS 预检:

// Spring Security 6.x 写法 import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.http.HttpMethod; @Configuration public class SecurityConfig { @Bean public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { http.authorizeHttpRequests(auth -> auth .requestMatchers(HttpMethod.OPTIONS, "/**").permitAll() .anyRequest().authenticated()) .cors(); return http.build(); } }

最后那个.cors()也很关键,它会启用 Spring Security 内置的 CORS 处理,集成项目里建议保留。如果放行 OPTIONS 后还是 403,再检查 CorsFilter 是否注册在 Security 过滤器链之前,顺序反了照样先被拦。这是集成 Security 的项目里最容易漏的两步。

5.4 现象:前端打包放进 Spring Boot 后还是报跨域

vue 项目打包后放进 src/main/resources/static 目录,浏览器访问的是同一个端口,此时页面和接口同源,理论上不存在跨域。如果生产环境还报跨域,大概率是接口地址仍然写死了开发环境的主机和端口。解决方法是同源部署时把接口请求改成相对路径,比如 /api/users 而不是 http://localhost:8081/api/users。开发环境里也别依赖 CORS,直接用 vite 或 webpack 的 proxy 把 /api 代理到后端端口,这样开发和生产都不会触发浏览器跨域检查。路由前缀要确保和 Controller 的 RequestMapping 对得上,否则接口 404 会被误判成跨域问题,我在不少项目里见过这两件事混在一起排查的场面。

5.5 现象:Nginx 二层代理:响应头被覆盖或丢失

后端配好了 CORS,前端却仍然报错,打开响应头发现有两个 Access-Control-Allow-Origin,或者完全没有。原因通常是 Nginx 层又加了自己的 add_header,覆盖了后端的值;或者 Nginx 的 proxy 配置里没有转发 OPTIONS 请求,预检在 Nginx 层就被拦截了。Nginx 的 invalid cors request 报错,多半是配置里用 if 判断 Origin 变量时逻辑写错,变量没取到导致条件分支异常。解决原则是“后端配了 Nginx 就别再配”,二选一。推荐在后端做,Nginx 只做反代:

location /api/ { proxy_pass http://springboot; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; }

如果一定要在 Nginx 层做跨域放行,那就把后端全局 CORS 配置去掉,只保留 Nginx 的 add_header,避免重复头。两边同时配置是踩坑重灾区,出现魔改式跨域问题时优先查这个。

6. 验证手法与进阶:curl 模拟预检请求和生产 CORS 检查清单

配置写完不是终点,验证才是。最稳的验证工具不是浏览器,而是 curl 模拟预检请求。它能精确控制 Origin 头,直接看到后端端到端返回的响应头,绕开浏览器缓存造成的假象。

6.1 用 curl 模拟 OPTIONS 预检请求

curl -i -X OPTIONS \ -H "Origin: http://localhost:8080" \ -H "Access-Control-Request-Method: POST" \ -H "Access-Control-Request-Headers: Content-Type,Authorization" \ http://localhost:8081/api/users

命令解析:-X OPTIONS说明这次请求是预检;-H "Origin: http://localhost:8080"模拟前端页面的源;Access-Control-Request-Method和Access-Control-Request-Headers模拟浏览器即将发起的真实请求形态。重点关注返回头里的四个字段:Access-Control-Allow-Origin 是否精确等于 http://localhost:8080,Allow-Methods 是否包含 POST 和 OPTIONS,Allow-Headers 是否覆盖 Content-Type 和 Authorization,Max-Age 缓存是否生效。如果 Origin 不在白名单里,响应头中根本不会出现 Allow-Origin,这时候直接回到配置白名单里检查。

还可以反向验证安全性,把 Origin 换成一个未配置的源:

curl -i -X OPTIONS \ -H "Origin: http://evil.example.com" \ -H "Access-Control-Request-Method: POST" \ http://localhost:8081/api/users

正常应该看不到 Access-Control-Allow-Origin 响应头。这个命令我每次部署前都会跑一遍,比上线后让前端同事点页面要高效得多。

6.2 生产 CORS 检查清单与进阶技巧

最后给一张我每次上线前对照检查的清单:

检查项通过标准
白名单配置无*,用 allowedOriginPatterns 精确到子域
Cookie 凭证需要登录态时确保 allowCredentials 与前端 withCredentials 一致
OPTIONS 放行Spring Security 与 Nginx 均不拦截预检请求
maxAge 缓存至少 3600,减少重复预检
响应头唯一后端与 Nginx 只保留一处 CORS 配置

一些补充的选择经验:CorsFilter 适合过滤器链复杂、WebMvcConfigurer 加不上头的情况;@CrossOrigin 适合单接口按需开跨域;全局配置适合绝大多数前后端分离项目。我个人的习惯是保留一份全局配置,接口级别除非有特殊白名单需求,否则不叠加注解,这样排查响应头的时候心智负担最小。

做这行几年,跨域配置是少有的“配置量不大但折磨人”的问题。我自己的教训是:别凭记忆写,也别在浏览器报错之后只看控制台不看响应头。每次先在本地用 curl 把预检请求和实际请求都打一遍,确认响应头完整,再放给前端联调。这套流程帮我省下的联调时间远超配置本身花的时间。希望帮到你。

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

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

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

立即咨询