ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

Spring Boot跨域解决方案:四种CORS配置方式详解

Spring Boot跨域解决方案:四种CORS配置方式详解 前后端分离的项目做多了跨域这个问题基本躲不开。前端跑在 localhost:5173后端 Spring Boot 跑在 8080浏览器一打开控制台就是红色报错。每年新入职的同事几乎都会来问一次代码明明没问题接口也能通为什么浏览器就是不让拿数据跨域的本质是浏览器的同源策略在起作用。简单说浏览器只允许页面请求同源协议、域名、端口都相同的资源一旦不同源请求发得出去响应也回得来但浏览器会拦着不让你的 JavaScript 读取。很多新手抓包看着响应里明明有数据又觉得是浏览器的问题其实后端只解决了“响应”没解决“让浏览器放行”的问题这就是 CORS 存在的意义。这篇文章就针对 Spring Boot 项目把解决跨域的四种常见方式完整过一遍注解方式、全局配置方式、过滤器方式以及整合 Spring Security 时的配置方式。每种我都会给出实际代码、原理分析和踩坑记录适合刚接触跨域的前端同学也适合后端想彻底搞明白配置逻辑的人。看完你应该能根据项目情况直接选一种用真出了问题也知道去哪里排查。1. 跨域问题到底是怎么来的1.1 同源策略是浏览器立的规矩同源策略是浏览器最基础的安全机制之一。所谓同源指协议、域名、端口三样完全一致。比如页面的地址是http://localhost:5173页面里发请求到http://localhost:8080/api/user端口不一样属于跨域。更直接的例子页面是https://a.example.com请求http://a.example.com/api协议不一致也是跨域。完整同源需要同时满足三个条件。任何一个不一致浏览器都会视为跨域请求。这是浏览器故意设计的核心目的是防止恶意网站通过脚本偷偷向其他站点发起请求比如拿到用户在其他网站登录态的 Cookie 然后伪造操作。如果完全放开表单提交、Ajax 请求都会被利用来做坏事整个 Web 生态的基本信任边界就没法建立了。有个有意思的点很多人问“origin 是源头的意思为什么 cross-origin 就翻译成跨域”。origin 在浏览器语境里指页面的来源协议域名端口跨域翻译其实对应的是“跨越来源”这个动作本身。看到英文文档里写 Cross-Origin Request就知道这是所有跟源不一致相关的请求国内习惯统一叫跨域。理解到这一层后面看 CORS 配置里的 origin 相关参数就不会发怵。1.2 CORS 的完整流程简单请求与预检请求CORSCross-Origin Resource Sharing是跨域问题的官方解决方案。它的思路不复杂浏览器在发起跨域请求前或过程中通过一组 HTTP 头字段让服务器明确声明“允不允许这个来源访问”。服务器配合了浏览器就放行服务器不配合浏览器就拦。CORS 把请求分成两种。一种叫简单请求满足条件比较苛刻请求方法是 GET、HEAD、POST 之一且 Content-Type 仅限于application/x-www-form-urlencoded、multipart/form-data、text/plain等有限的几个值。简单请求不会提前探测浏览器会直接发出真实请求把请求来源塞到Origin头里然后看响应里有没有Access-Control-Allow-Origin。另一种是预检请求。只要请求方法不是简单方法或者请求头里带了自定义字段比如常见的Authorization、X-Custom-Header或者 content-type 是application/json浏览器就会先发一个OPTIONS请求过去问服务器“我是这种来源我要用这种方法和这些头你允许吗”服务器返回允许的响应后浏览器才会发真正的业务请求。很多后端刚接触跨域时会觉得“我后端接口明明能收到 OPTIONS 请求但前端还是报跨域”“我这请求发过去响应正常前端就是解析不了”基本都是没理解预检请求机制导致的。后端得先答应 OPTIONS浏览器才把 POST 放行。1.3 为什么后端必须做点什么有人可能想既然跨域是浏览器的策略那我后端返回响应不就完了凭什么要后端配置因为浏览器只负责拦截真正要“许可”的是服务器。服务器需要显式返回 CORS 相关响应头比如Access-Control-Allow-Origin告诉浏览器这个来源是被许可的。服务器不返回这些头浏览器就直接阻断页面里拿不到任何数据。还有一类场景后端接口需要读取 Cookie 或认证信息。跨域请求默认不携带 Cookie就算带了浏览器也不会接受服务器的 Set-Cookie 结果。要让 Cookie 在跨域请求中生效必须在服务器响应头里加上Access-Control-Allow-Credentials: true。所以后端在跨域这件事上不是“可做可不做”而是必须明确表态否则接口永远只能对同源页面友好。2. 方式一CrossOrigin 注解快速放行2.1 注解的最简用法Spring Boot 里最简单粗暴的跨域方案是在 Controller 类或方法上加CrossOrigin注解。比如有个用户接口CrossOrigin(origins http://localhost:5173) RestController public class UserController { GetMapping(/api/user) public User getUser() { return userService.getUser(); } }不加任何参数也是可以的RestController public class UserController { CrossOrigin GetMapping(/api/user) public User getUser() { return userService.getUser(); } }不加参数表示允许所有来源生产环境风险较高不建议直接用。加了origins参数后Spring 会自动在响应头里写入对应的Access-Control-Allow-Origin并且处理预检请求。这个方法在快糙猛的场景下很实用比如临时调试、写 Demo、内部小系统加一个注解就通了不用新建配置类。2.2 注解的参数说明CrossOrigin注解里比较常用的参数就几个origins允许的来源列表比如http://localhost:5173多个来源用逗号分隔。allowedMethods允许的请求方法比如{GET, POST, PUT, DELETE}。如果接口里用了 PUT、DELETE预检时没声明浏览器也会拦。allowedHeaders允许的自定义请求头常见需要声明Authorization、Content-Type、X-Requested-With等。allowCredentials是否允许携带 Cookie默认 false。如果设为 trueorigins不能写*必须指定具体来源。maxAge预检请求的结果能缓存多少秒单位是秒。设置后浏览器在一段时间内不用再发 OPTIONS能减少无谓的预检开销。写全的版本长这样CrossOrigin( origins http://localhost:5173, allowedMethods {GET, POST, PUT, DELETE}, allowedHeaders *, allowCredentials true, maxAge 3600 ) RestController public class UserController { // ... }这个配置对当前 Controller 下的所有接口生效。如果只给某个方法加就只对那一个接口生效粒度更细。2.3 注解方式的局限性与适用场景注解方式最大的优点就是快缺点也很明显它属于局部配置散落在各个 Controller 里。项目里几十个 Controller你不可能挨个注解去检查漏掉一个接口前端就多一个跨域报错。另一个问题是不能统一维护跨域策略。今天允许的是 5173明天要多加一个 5174 端口你得全局搜注解一个个改改漏一个线上就出问题。注解方式也访问不到底层配置的更多细节。比如你想对不同的路径应用不同的允许规则注解就做不到了。所以我的实际建议是单机 Demo、临时联调、一次性工具类项目可以放心用注解但正式的多模块项目尽量别走这条路。后端的跨域配置应该像数据库配置、缓存策略一样是一个集中管理的关注点。3. 方式二全局CORS配置实现WebMvcConfigurer3.1 写法与核心参数全局配置是 Spring Boot 项目里最常用的方式。核心是写一个配置类实现WebMvcConfigurer接口重写addCorsMappings方法Configuration public class CorsConfig implements WebMvcConfigurer { Override public void addCorsMappings(CorsRegistry registry) { registry.addMapping(/**) .allowedOriginPatterns(http://localhost:*) .allowedMethods(GET, POST, PUT, DELETE, OPTIONS) .allowedHeaders(*) .allowCredentials(true) .maxAge(3600); } }addMapping(/**)表示对所有接口路径生效。allowedOriginPatterns支持通配符写法比如http://localhost:*可以匹配本地任意端口这在开发阶段很好用。如果严格限定线上域名可以写成.allowedOrigins(https://admin.example.com)。allowedMethods里建议把OPTIONS加上。虽然 Spring 对预检请求有内置处理但自定义配置里声明完整的方法列表更稳妥。allowedHeaders(*)表示允许浏览器在预检请求里携带任何自定义头实际项目中基本都这么配。allowCredentials(true)则配合携带 Cookie 的场景使用。3.2 和注解方式相比有什么优势全局配置最大的价值是集中管理。一个配置类控制整个项目的跨域策略前端加了新域名、改了端口后端只需要改这一处不用满项目翻注解。而且addMapping(/**)天然覆盖所有接口包括未来新增的 Controller不会出现“新接口忘了配跨域”这种低级事故。另一个优势是能基于路径灵活区分。比如/api/public/**允许所有来源/api/admin/**只允许内部管理后台域名访问不同业务模块用不同规则registry.addMapping(/api/public/**) .allowedOrigins(*) .allowedMethods(GET); registry.addMapping(/api/admin/**) .allowedOrigins(https://admin.example.com) .allowedMethods(GET, POST) .allowCredentials(true);这种写法配合后端微服务网关会更方便。各个服务都接入同一个 CORS 配置类规则统一审计也简单不用像注解那样东一个西一个地排查。3.3 常见坑allowedOriginPatterns vs allowedOrigins用全局配置时最经典的坑是allowedOrigins(*)与allowCredentials(true)冲突。按 CORS 规范两者同时出现是允许的但很多浏览器已经把Access-Control-Allow-Origin: *Access-Control-Allow-Credentials: true视为不安全配置会直接拒绝响应。Spring 在不同版本里的处理也有差异较新的 Spring Boot 版本里你如果尝试.allowedOrigins(*).allowCredentials(true)启动时可能直接报IllegalArgumentException。解决方法是使用allowedOriginPatterns。它可以写通配符模式并且和allowCredentials(true)兼容registry.addMapping(/**) .allowedOriginPatterns(*) .allowCredentials(true);注意一点allowedOriginPatterns(*)虽然也是允许所有来源但响应头里返回的不是*而是请求方实际的 Origin 值。浏览器会把动态回显的 Origin 当作明确许可因此可以和安全要求兼容。还有个小细节如果你只配置了allowedOrigins(http://localhost:5173)预检请求会成功但实际请求的响应头出现Access-Control-Allow-Origin: http://localhost:5173是正常的别看到响应头里不是*就觉得配置没生效。字段值本来就该指向具体来源。4. 方式三CorsFilter 过滤器方案4.1 基于 Spring 的 CorsFilter 注册第二种全局配置利用的是 Spring MVC 的拦截机制底层最终还是靠过滤器处理预检和响应头。如果你想更早介入请求链路或者你的项目不是标准 Spring MVC 结构可以直接注册CorsFilter。用法是定义CorsConfigurationSource并注册为 BeanConfiguration public class CorsFilterConfig { Bean public CorsFilter corsFilter() { CorsConfiguration config new CorsConfiguration(); config.addAllowedOriginPattern(*); config.addAllowedMethod(*); config.addAllowedHeader(*); config.setAllowCredentials(true); config.setMaxAge(3600L); UrlBasedCorsConfigurationSource source new UrlBasedCorsConfigurationSource(); source.registerCorsConfiguration(/**, config); return new CorsFilter(source); } }这段代码创建了一个CorsFilter并把它注册进 Spring 容器。UrlBasedCorsConfigurationSource的作用跟CorsRegistry类似都是把配置规则绑定到路径上/**表示所有路径。有些项目里能见到更底层的写法直接继承OncePerRequestFilter手工设置响应头Component public class CustomCorsFilter extends OncePerRequestFilter { Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { response.setHeader(Access-Control-Allow-Origin, request.getHeader(Origin)); response.setHeader(Access-Control-Allow-Methods, GET, POST, PUT, DELETE, OPTIONS); response.setHeader(Access-Control-Allow-Headers, Content-Type, Authorization); response.setHeader(Access-Control-Allow-Credentials, true); if (OPTIONS.equalsIgnoreCase(request.getMethod())) { response.setStatus(HttpServletResponse.SC_OK); return; } filterChain.doFilter(request, response); } }这种写法直观但我不建议直接复制进正式项目。因为手工处理请求头很容易遗漏边界情况比如 Origin 为空时响应头会被设置成null又比如预检请求直接 return 会绕过业务过滤器可能引入安全问题。既然 Spring 已经封装好了CorsFilter直接用它就好没必要重复造轮子。4.2 过滤器顺序问题深入过滤器方案有个绕不开的点过滤器执行顺序。如果项目里还有别的过滤器比如登录鉴权过滤器、日志过滤器它们的执行顺序取决于Order注解或者注册时的排序。CorsFilter必须尽量排在前面否则预检请求可能还没到 CORS 过滤器就被鉴权拦截了。实际表现就是前端请求带了自定义头浏览器先发 OPTIONS 预检结果后端鉴权过滤器要求所有请求都必须登录OPTIONS 请求没带登录凭证直接被拒绝返回 401浏览器显示跨域失败。这个问题排查起来很迷惑看起来像跨域配置有问题其实是过滤器顺序不对。解决方法有两种。一种是给CorsFilterBean 设置Order(Ordered.HIGHEST_PRECEDENCE)Bean Order(Ordered.HIGHEST_PRECEDENCE) public CorsFilter corsFilter() { // ... }另一种是结合FilterRegistrationBean显式指定顺序Bean public FilterRegistrationBeanCorsFilter corsFilterRegistration() { FilterRegistrationBeanCorsFilter registration new FilterRegistrationBean(); registration.setFilter(corsFilter()); registration.addUrlPatterns(/*); registration.setOrder(1); return registration; }如果你用了 Spring Security情况还要更复杂一点下一节会专门展开。4.3 什么情况下必须用 CorsFilter全局WebMvcConfigurer配置已经覆盖了 90% 的 Spring Boot Web 项目需求所以什么时候非用CorsFilter不可我总结了几类场景项目里不是标准 Spring MVC。比如某些技术栈用到了非 MVC 的 Servlet 链路CorsRegistry不生效。跨域规则需要和 Spring Security 的过滤器链深度集成。Security 的http.cors()底层用的就是CorsFilter你直接把CorsConfigurationSource暴露为 Bean 更省事。网关服务或微服务边缘节点要统一处理所有下游接口的跨域不想在每个服务内部各自配置。你从老项目迁移过来原来用的是过滤器方式保持一致维护成本最低。判断依据很简单先跑全局配置跑不通再换过滤器。不要一开始就上过滤器因为全局配置更符合 Spring Boot 的自动装配风格排查问题也有更多现成文档。5. 方式四Spring Security 整合 CORS5.1 Security 环境下为什么直接配 CORS 不生效很多项目在引入 Spring Security 后会发现之前调好的全局跨域配置突然失效了前端请求照样报跨域。原因是 Spring Security 的过滤器链会先于业务处理把请求拦下来如果 Security 这边没有明确启用 CORS它可能直接处理掉预检请求或者给响应加上了自己的一套逻辑导致你配置的WebMvcConfigurer根本没机会生效。另外 Security 的安全策略默认对跨域请求管控严格尤其在开启了 CSRF 防护时所有非简单请求可能被拒绝。你需要在 Security 配置里显式声明允许 CORS并告知它使用哪一个CorsConfigurationSource。否则两边各配各的就像两个保安都在管同一扇门谁也不承认对方的放行条。5.2 正确配置cors() CorsConfigurationSourceSpring Security 6 之后的整合方式比较简洁。先定义一个CorsConfigurationSourceBean再在SecurityFilterChain里启用cors()Configuration EnableWebSecurity public class SecurityConfig { Bean public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { http .cors(Customizer.withDefaults()) .csrf(Customizer.withDefaults()) .authorizeHttpRequests(auth - auth .requestMatchers(/api/public/**).permitAll() .anyRequest().authenticated() ) .httpBasic(Customizer.withDefaults()); return http.build(); } Bean public CorsConfigurationSource corsConfigurationSource() { CorsConfiguration config new CorsConfiguration(); config.addAllowedOriginPattern(*); config.addAllowedMethod(*); config.addAllowedHeader(*); config.setAllowCredentials(true); config.setMaxAge(3600L); UrlBasedCorsConfigurationSource source new UrlBasedCorsConfigurationSource(); source.registerCorsConfiguration(/**, config); return source; } }核心是两句配置类里显式声明CorsConfigurationSourceBean然后http.cors(Customizer.withDefaults())。这里withDefaults()的意思是让 Security 自动寻找容器里唯一的CorsConfigurationSourceBean 来使用。如果你只写了http.cors()但没有这个 BeanSpring 会尝试找 Spring MVC 那边的配置很容易出现两者不匹配。使用 Spring Security 的一体化配置时CorsConfigurationSource和 Spring MVC 的CorsRegistry二选一即可没有必要同时配。同时配容易出现重复响应头浏览器收到两个Access-Control-Allow-Origin虽然值一样不会报错但排查问题时容易产生干扰。更推荐的做法是把跨域规则统一放到CorsConfigurationSource里因为CorsFilter和 Security 两边都能识别这份配置兼容性最好。5.3 Security 与 CorsFilter 同时存在的顺序处理如果项目里已经手动注册了CorsFilterBean同时又加了 Spring Security过滤器的执行顺序就非常关键。Security 的过滤器链本身是通过FilterChainProxy管理的而CorsFilter要放在FilterChainProxy之前还是之后取决于你想让跨域处理覆盖到什么范围。由于 Spring Security 会拦截绝大部分请求链保护之前的过滤器可能根本没机会处理 OPTIONS 预检。所以实际经验是让CorsFilter在 Security 之前执行这样才能保证预检请求不会被 Security 的认证逻辑拦截。实现方式仍然是给CorsFilterBean 设置最高优先级Bean Order(Ordered.HIGHEST_PRECEDENCE) public CorsFilter corsFilter() { // ... }如果 Security 配置里已经用了http.cors()那这个手动注册的CorsFilter可以去掉避免双重处理。这两种方式选一个就行。真遇到需要兼顾的场景把CorsFilter的顺序排在 Security 过滤器链之前是一个比较稳妥的安排。6. 四种方式对比与选型建议6.1 对比表格这四种方式本质上都是在往响应里加 CORS 响应头、处理预检请求但适用场景和维护成本差别挺大。我用一个表格把核心区别列出来方式配置位置配置粒度维护成本适用场景CrossOriginController 类或方法类/方法级高分散在各层简单项目、临时联调、快速验证WebMvcConfigurer全局配置配置类集中管理路径级低一处修改全局生效常规 Spring Boot Web 项目优先推荐CorsFilter配置类注册 Bean路径级支持过滤链最前端处理低需要关注过滤器顺序非标准 MVC、网关统一处理、复杂过滤器链Security 整合配置Security 配置类 CorsConfigurationSourceBean安全过滤链级低但需要理解 Security 链路项目已集成 Spring Security必须使用选型的核心不是看谁先进而是看项目里有没有 Spring Security。有 Security直接用第四种没有 Security优先用第二种只有一两个接口要紧急放行临时用第一种没问题后期记得改成全局配置。6.2 我的实际选型经验经手过几个逐步演进的项目后我的固定做法是这样的项目一上来就规划了 Spring Security跨域直接在 Security 配置里统一管后端所有微服务都把 CORS 配置收敛到一个公共依赖模块里避免每个服务各写一份。新增服务时引依赖就能获得一致的跨域策略前端联调基本不用后端配合改代码。没有 Security 的内部系统直接用WebMvcConfigurer全局配置代码量最少团队理解成本最低。等以后要引入 Security把配置迁移成CorsConfigurationSource也很轻松结构基本不变。我还遇到过另一种情况公司安全规范要求跨域配置全部由网关统一处理后端各服务一律不配 CORS。这时候服务端本身就不该有任何 CORS 配置所有规则都在网关层生成响应头。这种架构下你就也别纠结选什么方式了既然服务端不参与只需要保证代码里没有残留的CrossOrigin注解即可。7. 常见问题与排查技巧实录7.1 预检请求失败排查跨域排查最常遇到的就是 OPTIONS 预检失败。前端控制台报Access to XMLHttpRequest at xxx from origin xxx has been blocked by CORS policy后面通常会跟一句Response to preflight request doesnt pass access control check。排查步骤先确认预检请求本身的响应。打开浏览器开发者工具切到 Network筛选 OPTIONS 请求看响应头里有没有Access-Control-Allow-Origin。如果整个 OPTIONS 请求直接报 404、401、403那问题往往不在跨域配置而是过滤器链或者路由把预检请求拦了。常见原因有三个登录鉴权过滤器把 OPTIONS 请求拦了没放行预检。Spring Security 的 CSRF 防护把非简单请求拦了没有配置忽略预检。路径匹配问题比如addMapping只配了/api/**但请求实际路径是/other/**。用 curl 直接模拟预检请求是最快的验证方式curl -i -X OPTIONS http://localhost:8080/api/user \ -H Origin: http://localhost:5173 \ -H Access-Control-Request-Method: GET看到响应头里有Access-Control-Allow-Origin: http://localhost:5173说明后端放行逻辑本身没问题问题可能出在浏览器缓存或者前端代理层。7.2 带 Cookie 的请求处理跨域请求如果需要携带 Cookie前后端得同时配合。后端设置allowCredentials(true)并且不能用allowedOrigins(*)要改成allowedOriginPatterns(*)或明确指定域名。前端如果是 axios需要配置withCredentials: true。否则即使后端正确返回了Access-Control-Allow-Credentials: true浏览器也不会在请求里带 Cookie。还有个容易忽略的细节Access-Control-Allow-Origin响应头在带凭据请求下不能是*必须是具体来源值。如果浏览器报错提示The value of the Access-Control-Allow-Origin header in the response must not be the wildcard *基本就是后端配置里把allowedOrigins(*)和凭据一起用了马上改成allowedOriginPatterns。7.3 前端代理是不是可以完全替代后端 CORS开发环境下很多人用前端代理解决跨域。Vite 的server.proxy、Webpack 的devServer.proxy都可以把/api请求转发到后端浏览器看到的是同源请求自然就不需要 CORS。这种方式开发时很好用但生产环境就失效了。生产环境如果前端静态资源部署在 CDN 或独立域名请求还是会直接到后端域名跨域依然存在。而且一旦前端用了代理后端的跨域配置在生产环境一样要全量生效代理并没有减少后端的工作量只是把开发环境的问题藏起来了。所以我的建议是开发环境用代理当然可以但后端该配的跨域还是要配。不然前端上线时发现生产环境跨域报错临时去加配置再来一次回归测试完全没必要。7.4 开发环境与生产环境配置策略推荐的做法是配置里允许开发环境的规则宽松一些生产环境严格一些。开发环境允许所有来源、支持通配符、允许任何自定义头方便联调registry.addMapping(/**) .allowedOriginPatterns(*) .allowedMethods(*) .allowedHeaders(*) .allowCredentials(true) .maxAge(3600);生产环境尽量把来源白名单收敛到具体域名registry.addMapping(/**) .allowedOrigins(https://admin.example.com, https://www.example.com) .allowedMethods(GET, POST, PUT, DELETE) .allowedHeaders(Content-Type, Authorization) .allowCredentials(true) .maxAge(3600);白名单写死的好处是安全审计方便也避免第三方网站借机调用内部接口。多环境切换可以用 Spring 的 profile 机制把不同配置放到application-dev.yml和application-prod.yml里再绑定配置项。不过也别过度设计小项目一个配置类里多写几行代码就够了没必要搭一套动态配置框架。跨域问题在 Spring Boot 里不算难题但接手的项目多了你会发现十个跨域报错里五个都是配置位置不对、过滤器顺序不对、或者 Security 配置忘开 CORS真正不懂原理的人很少。先把四种方式各自适用什么场景搞明白再根据项目情况选一个落地出问题时沿着请求链路从头看一遍 OPTIONS 和响应头大部分坑都能自己解决。我在实际项目里最常用的组合就是全局配置加 Security 整合配置平时顺手维护一份生产环境白名单前端同事基本很少再来找我了。
返回列表