
前端同事又在群里喊接口跨域了后端拿 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 简单请求与预检请求CORSCross-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.2Spring 框架版本开始支持 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是否允许携带 CookiefalsemaxAge预检请求结果的缓存时间秒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 的 DelegatingFilterProxyCorsFilter 应该放在足够靠前的位置确保跨域响应头最先加上。用 FilterRegistrationBean 注册时可以通过 setOrder 控制顺序Bean public FilterRegistrationBeanCorsFilter corsFilterRegistration(CorsFilter corsFilter) { FilterRegistrationBeanCorsFilter 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 头。遇到这类问题可按下面的链路一步步排查看浏览器 Network 面板确认是否有 OPTIONS 预检请求。没有预检请求说明请求是简单请求或者浏览器没开 CORS。如果 OPTIONS 返回 401/403先确认 Security 是否已经把/preflight 相关的 URL 放行。预检请求通常不带认证信息如果被 Security 当作未认证请求拦截就回不到业务层了。必要时为 OPTIONS 请求放行.requestMatchers(HttpMethod.OPTIONS, /**).permitAll()这一步属于临时手段真正稳妥的还是靠 .cors() 让 Security 在过滤器链上先把预检请求处理掉而不是交到认证逻辑里去。看响应头。如果真实请求的响应里有 Access-Control-Allow-Origin但浏览器还是报错多半是 allowCredentials 与 allowedOrigin 匹配的问题或者暴露头不足。后端日志看不出毛病那就用 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 报错也能顺着响应头和请求链路一步步定位而不是病急乱投医。