尧图精选

Spring Security 跨域问题全解析:从过滤器链到 CORS 正确配置

🕒 发布时间:2026/9/10 9:15:59 📁 来源:尧图网络
前一阵子有个读者跑来问我后端接口在 Swagger 里测试全通前端一联调就报跨域后端代码里CrossOrigin也加了为什么还是不行我让他把 Spring Security 的过滤器链截图发过来一看就明白了。SpringSecurity 里的跨域跟普通 SpringBoot 项目的跨域配置有本质区别。如果你只会在 Controller 上加注解、或者配一个WebMvcConfigurer在 Security 环境下大概率会掉坑里。这篇文章我把这些年处理 SpringSecurity 跨域问题的经验、代码、踩坑记录全部整理出来从同源策略讲到过滤器链顺序从CrossOrigin失效原因讲到生产环境最推荐的配置方式保证你看完能直接上手。1. 跨域的根子在哪SpringSecurity 为什么总会“插一脚”1.1 同源策略不是后端限制是浏览器行为很多人把“跨域报错”当成后端的问题其实这是浏览器做的安全校验。所谓“同源”就是协议、域名、端口三者完全一致只要有一个不同浏览器就会认为这是跨域请求。举例来说前端页面跑在http://localhost:8080后端接口跑在http://localhost:9090端口不同属于跨域前端http://a.com请求http://b.com域名不同也属于跨域。浏览器发起这些请求后如果响应头里没有携带正确的 CORS 字段浏览器就会把响应“拦截”下来控制台报No Access-Control-Allow-Origin header is present。这里有个关键点请求可能已经到达后端后端也可能已经正常处理并返回了结果但浏览器不让 JS 读取这个结果。这就是为什么你用 Postman、Swagger 测试时一切正常一到浏览器里就报跨域的原因——因为那些工具不走浏览器的同源策略校验。1.2 简单请求和预检请求怎么区分CORS 请求分成两类处理方式不一样简单请求请求方法只允许GET、POST、HEAD且 Content-Type 只能是application/x-www-form-urlencoded、multipart/form-data、text/plain中的一种同时不能带自定义请求头。这类请求浏览器会直接发出去响应时检查 CORS 头。预检请求只要带自定义 Header比如Authorization、或者 Content-Type 是application/json、或者方法不是简单方法浏览器就会先发一个OPTIONS请求问服务器“允不允许我这么干”服务器返回允许后浏览器才会发真正的业务请求。前后端分离项目里POST JSON 是常态还要带 Token所以几乎所有请求都会触发预检。如果预检请求没有正确响应浏览器根本不会发后续的真实请求。1.3 Security 过滤器链在 CORS 处理之前就把请求拦了这是 SpringSecurity 跨域问题最核心的根源。Spring MVC 处理跨域实际是在DispatcherServlet分发之后通过HandlerInterceptor或CrossOrigin注解在方法执行前给响应设置 CORS 头。但 Spring Security 是 Servlet 容器层面的过滤器链它比你整个 MVC 链路都要靠前。想象一下这个执行顺序浏览器预检请求进来 - Spring Security 过滤器链认证、授权逻辑 - 如果没通过直接返回 401/403根本走不到 DispatcherServlet - 响应头里没有 CORS 字段 - 浏览器判定跨域失败。也就是说当你的接口需要登录才能访问时Spring Security 会在你跨域配置还没有机会执行之前就返回了错误响应。前端看到的是 CORS 报错但本质是接口已经被 Security 拦截了只是拦截后的响应缺少 CORS 头浏览器把这口锅算在了“跨域”头上。明白了这个链路你就能理解为什么CrossOrigin在 SpringSecurity 环境下经常会失灵。1.4 后端跨域和前端跨域到底该谁来解决关于跨域业界一直有两种方向后端配置 CORS 响应头或者前端通过代理比如 Nginx 反向代理、Vue DevServer Proxy让浏览器觉得请求是同源的。两种方案都是合法的但如果你后端接了 Spring Security我建议优先走后端正确配置 CORS而不是教前端去开代理。原因很简单你无法控制所有调用方的环境有时候是 H5 原生页面直接调接口有时候是第三方系统内置 WebView 调接口前端代理在这些场景下不一定可用。后端把 CORS 响应头配好所有浏览器环境都能兼容。JSONP、iframe postMessage 那些方案我也在后文提一下但不是首选因为 JSONP 只支持 GET、无法处理 RESTful 风格接口在现在的项目里基本是历史方案了。2. 基础跨域配置为什么在 Security 下不生效2.1 CrossOrigin 注解的适用边界CrossOrigin可以直接加在 Controller 类上或方法上是很多人最先接触到的跨域配置。比如RestController RequestMapping(/api/user) CrossOrigin(origins http://localhost:5173) public class UserController { GetMapping(/info) public ResultUserInfo info() { return Result.success(userService.getInfo()); } }这个配置的本质是在当前 Controller 处理的响应上添加 CORS 响应头。它能解决的问题是请求能到达这个 Controller 且没有被更前置的拦截器拦截的情况下给响应补上跨域头。问题就出在这里。Spring Security 的过滤器链在整个 MVC 链路之前如果你的接口在 Security 那边就被判定为未认证、未授权根本就不会走到 ControllerCrossOrigin自然没有机会执行。你接口返回 401 时响应体里没有任何 CORS 头浏览器直接报跨域错误。所以CrossOrigin只适合两种情况一是项目没接 Spring Security二是接口本身就是匿名可访问的路径Security 放行了。一旦接口进入 Security 的保护范围它就成了摆设。2.2 WebMvcConfigurer 全局配置同样只能处理 MVC 层如果不喜欢在 Controller 上一个个加注解很多人会写一个全局配置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); } }这个配置在普通 SpringBoot 项目里是好用的因为它注册到了 HandlerMapping 的 CORS 处理逻辑里。但只要 Spring Security 在前面拦截了请求你配的这套东西同样没有执行机会。我在实际项目里见过有人加了这个配置又在 Security 里做了requestMatchers(/api/**).permitAll()放行结果接口能通就误以为是这个配置生效了。后来某个接口需要登录权限一收紧又跨域他才发现根本不是这个配置的功劳。2.3 失效的核心过滤器链顺序把上面的问题归结成一句话Spring Security 作为 Servlet Filter 运行在 DispatcherServlet 之前它有权在 CORS 处理逻辑执行前结束请求。图示大概是这样的浏览器请求 - Tomcat 容器 - springSecurityFilterChain - [认证过滤器] - [授权过滤器] - 若通过 - DispatcherServlet - HandlerMapping CORS 处理 - Controller - 响应 - 浏览器。CrossOrigin和WebMvcConfigurer的 CORS 处理都发生在 DispatcherServlet 之后的环节。所以要让跨域真正生效必须把 CORS 处理的“站位”提前放到 Spring Security 过滤器链之前或者链内靠前位置。这也是整篇文章最核心的方法论跨域配置要配合 Security 过滤器链的层级去处理而不是只盯着 MVC 那一层。3. 正确姿势在 SpringSecurity 过滤器链上挂 CORS3.1 核心配置CorsConfigurationSource 和 http.cors()Spring Security 从 4.2 开始就原生支持 CORS正确做法是提供一个CorsConfigurationSourceBean然后在HttpSecurity配置里调用http.cors()Security 会把它内置的CorsFilter放到过滤器链的靠前位置在认证、授权之前就把 CORS 头写好。先看一个完整的配置类import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.http.HttpHeaders; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; import org.springframework.security.web.SecurityFilterChain; import org.springframework.web.cors.CorsConfiguration; import org.springframework.web.cors.CorsConfigurationSource; import org.springframework.web.cors.UrlBasedCorsConfigurationSource; import java.util.List; Configuration EnableWebSecurity public class SecurityConfig { Bean public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { http // 启用 CORS会从容器中查找 CorsConfigurationSource Bean .cors(cors - {}) // 跨域无状态Token场景下建议关闭 CSRF理由后面讲 .csrf(csrf - csrf.disable()) .authorizeHttpRequests(auth - auth .requestMatchers(/api/auth/**, /error).permitAll() .anyRequest().authenticated() ) .formLogin(form - form.disable()) .httpBasic(basic - basic.disable()); return http.build(); } Bean public CorsConfigurationSource corsConfigurationSource() { CorsConfiguration config new CorsConfiguration(); // 这里不要用 *后面说明原因 config.setAllowedOriginPatterns(List.of( http://localhost:*, https://your-domain.com )); config.setAllowedMethods(List.of(GET, POST, PUT, DELETE, OPTIONS)); config.setAllowedHeaders(List.of(*)); config.setAllowCredentials(true); config.setMaxAge(3600L); UrlBasedCorsConfigurationSource source new UrlBasedCorsConfigurationSource(); source.registerCorsConfiguration(/**, config); return source; } }重点看.cors(cors - {})这一行。它做两件事第一向过滤器链注册一个CorsFilter位置在认证过滤器之前第二自动从 Spring 容器中查找CorsConfigurationSource类型的 Bean 来填充 CORS 策略。这意味着 CORS 响应头会在 Spring Security 做权限校验之前就写入响应。即使请求因为未认证被拦截返回的 401/403 响应里也会带Access-Control-Allow-Origin等头浏览器就能正常识别错误而不是误报跨域。前端此时看到的才是 401而不是那一堆 CORS error。3.2 为什么 allowedOrigin 不建议用 *以及 allowedOriginPatterns 的作用如果你把allowedOrigins直接配成*同时allowCredentials(true)在 Spring 5.3 以上版本会直接抛出异常When allowCredentials is true, allowedOrigins cannot contain the special value *原因很好理解Access-Control-Allow-Origin: *表示允许任意源携带请求但不能配合Access-Control-Allow-Credentials: true使用否则浏览器出于安全考虑会直接拒绝响应。而带 Cookie 的跨域场景几乎都需要allowCredentials(true)。allowedOriginPatterns是 Spring 5.3 引入的替代方案允许使用通配符模式匹配 Origin同时兼容allowCredentials(true)。它会根据实际请求的 Origin 动态生成具体的Access-Control-Allow-Origin响应头而不是简单返回*。所以我的建议是能用具体域名就用具体域名多个环境可以写多个 allowedOriginPatterns。实在要通配就配allowedOriginPatterns(*)而不是allowedOrigins(*)。3.3 到底要不要关闭 CSRF跨域配置经常会连带遇到 CSRF 的问题。默认情况下 Spring Security 开启 CSRF 防护对 POST、PUT、DELETE 等修改类请求要求携带 CSRF Token。前后端分离后如果你通过 Cookie 之外的 Header 传递 Token且不使用 Session那 CSRF 防护的意义就很弱反而会导致每一次 POST 请求都报 403 Forbidden。很多团队在引入 SpringSecurity 做 JWT 登录后会执行csrf(csrf - csrf.disable())这在跨域无状态 Token 的场景下是合理的。因为 CSRF 防御的核心逻辑是基于 Session Cookie 的身份认证你已经不用了继续开反而处处碰壁。如果出于某些原因必须保留 CSRF比如部分老功能依赖 Session你至少要保证跨域预检请求不受 CSRF 影响。可以在 Security 里对OPTIONS请求放行.authorizeHttpRequests(auth - auth .requestMatchers(HttpMethod.OPTIONS, /**).permitAll() .requestMatchers(/api/auth/**).permitAll() .anyRequest().authenticated() )同时如果使用 Cookie 传递 CSRF Token跨域下还需要配合CookieCsrfTokenRepository来写 Cookie复杂度会上升。我的建议是除非有硬性合规要求否则 JWT 无状态接口直接禁用 CSRF简单可靠。3.4 Spring Security 6.x 与 5.x 的写法差异上面的示例是 Spring Security 6.x / Spring Boot 3.x 的写法。如果你还在用 Spring Boot 2.x / Security 5.x有几处差异要注意authorizeHttpRequests在 5.5 之前是authorizeRequests写法是.antMatchers(...)。csrf().disable()在 6.x 里改成了csrf(csrf - csrf.disable())5.x 可以直接链式调用。5.4 之前SecurityFilterChain的写法可能要求继承WebSecurityConfigurerAdapter这是旧版最常见的一种写法。遇到报错时先看你的 Spring Boot 版本然后对症调整不要照抄网上的代码直接跑版本不匹配的问题很常见。4. 兜底方案和几种特殊场景4.1 自定义 CorsFilter 挂到最前面就算你正确配置了CorsConfigurationSource在某些极端情况下还是可能出问题最常见的是项目里引入了其他全局过滤器或者 Security 版本太老不支持http.cors()。这时候可以自己写一个CorsFilter放最前面。import org.springframework.core.Ordered; import org.springframework.core.annotation.Order; import org.springframework.stereotype.Component; import org.springframework.web.cors.CorsConfiguration; import org.springframework.web.cors.UrlBasedCorsConfigurationSource; import org.springframework.web.filter.CorsFilter; Component Order(Ordered.HIGHEST_PRECEDENCE) public class CustomCorsFilter extends CorsFilter { public CustomCorsFilter() { super(configurationSource()); } private static UrlBasedCorsConfigurationSource configurationSource() { CorsConfiguration config new CorsConfiguration(); config.setAllowedOriginPatterns(java.util.List.of(*)); config.setAllowedMethods(java.util.List.of(GET, POST, PUT, DELETE, OPTIONS)); config.setAllowedHeaders(java.util.List.of(*)); config.setAllowCredentials(true); config.setMaxAge(3600L); UrlBasedCorsConfigurationSource source new UrlBasedCorsConfigurationSource(); source.registerCorsConfiguration(/**, config); return source; } }注意类上的Order(Ordered.HIGHEST_PRECEDENCE)目的是让这个过滤器优先于 Spring Security 的过滤器链执行。Spring Boot 会把它注入到过滤器链最前端这样任何请求包括被 Security 拦截的请求都会先经过 CORS 处理。这个方式跟方案二并不冲突可以一起用也可以单独用。不过我要提醒一下这属于兜底方案正常项目建议优先用http.cors()CorsConfigurationSource因为那样 CORS 策略完全由 Spring Security 管理配置集中、可维护性高。自定义 Filter 适合老项目改造或者几个过滤器之间顺序不好控制的情况。4.2 OPTIONS 预检请求怎么放行跨域请求的预检是OPTIONS方法很多时候报错不是因为 CORS 配置不对而是OPTIONS请求本身在 Security 层面被拦截了。如果你用的是方案二http.cors()Spring Security 内置的CorsFilter实际上会直接拦截并处理OPTIONS预检请求——检测到是预检后它会返回 200 并写入 CORS 头不再往下传递。但如果你用了自定义 Filter 或者 Security 配置里还有额外拦截器就要小心预检请求被拦截。保险起见在authorizeHttpRequests里对OPTIONS做放行.authorizeHttpRequests(auth - auth .requestMatchers(HttpMethod.OPTIONS, /**).permitAll() .anyRequest().authenticated() )这段配置对任何情况都无害加了之后能省很多排查时间。4.3 反向代理和网关场景下的 CORS 该在哪一层配现在服务化架构很常见前端请求通常会先经过 Nginx 或 Spring Cloud Gateway 再转发到后端服务。这时候跨域处理位置有讲究。如果网关层做了统一 CORS 配置例如 Nginx 的add_header或 Gateway 的GlobalCorsConfiguration那么后端服务本身可以不配 CORS。这是因为浏览器实际请求的地址是网关地址只要网关返回 CORS 头即可。反向代理的本质是让浏览器认为请求发生在同源下如果代理配置正确甚至不会有跨域问题。如果网关没有配 CORS请求落到后端服务那后端 SpringSecurity 配置的 CORS 依然要生效但要注意响应头不能重复。我曾经遇到一个项目Nginx 配了add_header Access-Control-Allow-Origin *后端 SpringSecurity 也配了 CORS结果浏览器同时收到了两个不同值的同名响应头导致解析失败报错内容特别隐蔽。这类问题的排查方式是打开浏览器 F12 看响应头如果Access-Control-Allow-Origin出现两次就是多级都加了 CORS 配置。要么去掉一层要么保证每层的 Header 值一致。4.4 iframe、JSONP、3D 模型等历史方案的边界在自然延伸一下其他跨域场景。有些人还在用 JSONP 解决跨域它只能发 GET 请求并且需要后端接口改成 JSONP 回调格式我这边用到它的场景基本只有对接老系统的接口。iframe 跨域的问题靠的是postMessage通信适合嵌入第三方页面做消息传递而不是作为接口调用的替代方案。前端绘图、加载 3D 模型这块也经常踩跨域坑。比如用 Three.js 加载外部 3D 模型、使用 wasm 模块、加载远程图片纹理时如果静态资源服务器没有返回 CORS 头浏览器照样会拦截。像 l-painter 这类 canvas 绘制库请求远程图片资源时也会遇到同样的问题。这类问题跟 SpringSecurity 无关解决思路是给静态资源服务配 CORS或者通过本地代理转发让它变成同源请求。5. 高频踩坑与排查清单5.1 前端一调用就报 No Access-Control-Allow-Origin header最常见。按以下顺序排查看请求到底有没有发到后端。F12 Network 里如果看到OPTIONS状态是 200但POST状态是 403 或 401说明后端 Security 拦截了如果OPTIONS本身就报错说明 CORS 配置没生效。用 curl 模拟带 Origin 的请求直接看响应头curl -i -X OPTIONS http://localhost:8080/api/user/info \ -H Origin: http://localhost:5173 \ -H Access-Control-Request-Method: POST \ -H Access-Control-Request-Headers: Authorization,Content-Type重点看返回的响应头有没有Access-Control-Allow-Origin。如果有说明 CORS 配置没问题问题在后续的认证授权流程如果没有说明配置还没生效。检查 Spring Security 配置里有没有写.cors(cors - {})只写了CorsConfigurationSourceBean 是不够的必须在过滤链里开启。5.2 allowCredentials(true) 与 allowedOrigins(*) 冲突报错一般是java.lang.IllegalArgumentException: When allowCredentials is true, allowedOrigins cannot contain the special value *把setAllowedOrigins(List.of(*))改成setAllowedOriginPatterns(List.of(*))即可。如果你只需要支持固定域名直接写具体域名更好。5.3 预检请求返回 200但真实请求永远是 401/403这种情况最迷惑人因为OPTIONS是 200浏览器认为跨域规则没问题但业务请求还是被 Security 拦截了。原因一般是你的接口需要认证前端在OPTIONS通过后发起真实请求时带了Authorization头但 Token 过期、格式不对、或者 Security 的 filter 没有正确解析 Token。这是 SpringSecurity 本身的问题不是 CORS 的锅。排查方式F12 看真实请求的响应体里面写的是Authentication token was expired还是别的什么。如果响应体确实带了业务 JSON说明 Security 已经放行那就是 CORS 处理的问题如果响应体是 Security 默认的AuthenticationExceptionJSON问题在认证链路。5.4 联调环境正常一上生产就跨域这种情况通常是生产环境和开发环境的域名不一致。开发环境前端可能是http://localhost:5173生产环境变成了https://admin.example.com而后端配置里只允许了 localhost。排查方法看生产请求的Origin头是什么然后跟后端 CORS 配置里allowedOriginPatterns是否匹配。如果你的配置里写了http://localhost:*那就把生产域名也加进去。更稳妥的做法是配置成动态从配置文件读取Value(${app.cors.allowed-origins:http://localhost:*}) private String[] allowedOrigins;部署时按环境注入不会出现代码写死域名的问题。5.5 火狐和其他浏览器的本地限制火狐浏览器在本地开发时也经常出现跨域问题尤其是使用file://协议打开 HTML 页面时浏览器对跨域限制更严格。Chrome 和 Edge 相对宽松有时候本地开发起来能通火狐就报跨域。我的建议是本地开发不要用file://直接打开页面而是通过前端开发服务器的http://localhost:xxxx访问。如果碰到火狐本地测试报跨域优先考虑是不是把本地文件当成了 Origin而不是急着给后端加配置。也尽量不要通过关闭浏览器安全策略来绕过跨域这样会把本地环境搞得很奇怪而且无法模拟真实用户的问题。5.6 响应头出现重复的 Access-Control-Allow-Origin如果你用了 Nginx 转发、Gateway 网关或者同时配置了CorsFilter和http.cors()就可能出现两个相同的响应头。浏览器会取两个值做对比不一致时直接判定 CORS 失败。排查方法F12 看响应头的重复情况找到哪一层加了重复配置删掉一层。一般原则是网关/Nginx 配了就不要再让后端配后端配了就别让网关重复加。如果一定要都配保证生成的Access-Control-Allow-Origin值完全一致。6. 我的实际生产配置和一点经验最后把我最常用的一套生产配置贴出来算是做个总结。这套配置在 Spring Boot 3.x Spring Security 6.x 上跑了两个项目一直很稳定。Configuration EnableWebSecurity public class SecurityConfig { Bean public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { http .cors(cors - {}) .csrf(csrf - csrf.disable()) .sessionManagement(session - session.sessionCreationPolicy(SessionCreationPolicy.STATELESS)) .authorizeHttpRequests(auth - auth .requestMatchers(HttpMethod.OPTIONS, /**).permitAll() .requestMatchers(/api/auth/**, /actuator/health).permitAll() .anyRequest().authenticated() ) .addFilterBefore(jwtAuthenticationFilter(), UsernamePasswordAuthenticationFilter.class); return http.build(); } Bean public CorsConfigurationSource corsConfigurationSource() { CorsConfiguration config new CorsConfiguration(); config.setAllowedOriginPatterns(Arrays.asList( http://localhost:*, https://admin.example.com )); config.setAllowedMethods(Arrays.asList(GET, POST, PUT, DELETE, PATCH, OPTIONS)); config.setAllowedHeaders(Arrays.asList(*)); config.setAllowCredentials(true); config.setMaxAge(3600L); UrlBasedCorsConfigurationSource source new UrlBasedCorsConfigurationSource(); source.registerCorsConfiguration(/**, config); return source; } }几个关键点再强调一遍http.cors(cors - {})是必选项只定义CorsConfigurationSourceBean 不会自动生效。OPTIONS放行可以帮你免去预检请求被 Security 拦截的烦恼。allowedOriginPatterns代替allowedOrigins(*)同时配合allowCredentials(true)Cookie 和前端自定义 Header 都能正常工作。前后端分离 JWT 时关闭 CSRF 是合理的别被“CSRF 必须开启”的说法吓到。踩过几次坑之后我现在排查跨域问题的顺序已经很固定先确认请求是否真的到达后端再看响应头里 CORS 字段是否齐全最后检查 Security 过滤器链顺序。前端报的 CORS 错误八成是后端认证拦截导致的连锁反应不要一上来就改 CORS 配置先把 Security 的拦截逻辑捋清楚很多问题就自然解开了。
上一篇/下一篇内容由系统自动关联 返回资讯列表 →