☰
SpringSecurity跨域配置实战:解决CORS预检请求被拦截问题
2026/9/30 12:17:01 网站建设 项目流程

前后端分离的项目做多了,总能碰到这类问题:前端在浏览器里访问接口,控制台报错,说“CORS policy: No 'Access-Control-Allow-Origin' header is present”,或者请求直接被拦成红色。更坑的是,你在SpringMVC层面明明已经配置了跨域,前后端联调也正常,结果把SpringSecurity加进来后,跨域突然就废了,甚至OPTIONS请求直接给你返回403。这是我在好几个项目里踩过的坑,今天就把SpringSecurity场景下的跨域配置一次性说清楚。

这篇文章会覆盖CORS在SpringSecurity中的配置方式、为什么会出现“配置了但没生效”的假象、预检请求被拦截的根因,以及结合JWT场景的完整配置方案。适合正在做前后端分离项目、被跨域问题折腾过的同学参考,特别是那些已经把SpringSecurity集成进来、但还没搞明白跨域过滤器应该放在哪一层的人。

1. 先把跨域这事说透

1.1 同源策略和CORS到底在干什么

浏览器有一个铁律叫“同源策略”。所谓同源,是指协议、域名、端口三者完全一致。只有当这三个条件都满足时,页面里的JavaScript才被允许读取另一个资源的响应。比如前端跑在http://localhost:8080,后端跑在http://localhost:9090,这两个地址端口不一样,就属于跨域。

跨域不是后端限制的,而是浏览器限制的。后端接口本身是可以收到请求的,服务器照常处理,响应也正常返回了,但浏览器拿到响应后发现“响应头里没有允许我这个源访问的标记”,于是直接把响应拦截了,体现在控制台就是CORS报错。所以CORS配置的本质,是后端在HTTP响应头里声明“我允许哪些源来访问我的资源”,浏览器看到这个声明后,才放行给前端页面。

1.2 跨域配置为什么会跟SpringSecurity扯上关系

很多人最开始是只加了@CrossOrigin注解或者配置了WebMvcConfigurer里的CorsRegistry,在简单项目里是能跑通的。但一旦引入SpringSecurity,事情就变了。SpringSecurity的过滤器链是整个请求进入Controller之前的“关卡”,它先于SpringMVC的拦截器执行。如果你只在MVC层做了CORS配置,那么当请求带着跨域访问的意图进来时,SpringSecurity过滤器链可能已经把请求拦了,MVC层的CORS配置根本等不到上场。

SpringSecurity会拦截所有请求,包括预检请求OPTIONS。如果Security配置里没有放行OPTIONS,或者没有在Security层次启用CORS,那前端发起的预检请求会直接被拒绝,真正的请求自然也就无法执行。

1.3 预检请求:前端多出来的那次OPTIONS请求

当你的请求满足以下条件之一时,浏览器会先发送一个OPTIONS请求,这叫预检。条件包括:使用了非简单请求方法(如PUT、DELETE)、请求头包含自定义字段(如Authorization、X-TOKEN)、Content-Type不是简单的表单类型(如application/json)。

预检请求的作用,是问一下服务器“我准备发一个带Authorization头的PUT请求,你允许吗”。服务器需要针对这次OPTIONS请求返回合适的CORS响应头,浏览器确认无误后,才会发送真正的请求。SpringSecurity如果不处理这个预检请求,它就会被当成普通请求进入安全认证流程,没登录的人直接被拒。于是前端就只看到预检失败,状态码可能是403,但后端日志里根本看不到业务代码被触发。

2. SpringSecurity中配置CORS的完整方案

2.1 方案一:利用Spring Security自带的CorsFilter

从Spring Security 4.2开始,框架内部就集成了对CORS的支持。到了Spring Security 5时代,只要在SecurityFilterChain里调用http.cors(),Spring Security就会自动往过滤器链里挂一个CorsFilter。这个过滤器会从Spring容器中查找名为corsConfigurationSource的Bean,用它的配置来生成CORS响应头。

为什么我强调用Security层的方案?因为这样CORS的处理在过滤器链的最前端就完成了。请求先经过CorsFilter,预检请求在这里就直接响应终结,不会进入后面的认证授权流程。这样就不会出现“MVC配了CORS,但请求到不了MVC”的尴尬局面。

2.2 方案二:WebMvcConfigurer配置CorsRegistry

SpringMVC本身也提供跨域配置方式:

@Configuration public class WebConfig implements WebMvcConfigurer { @Override public void addCorsMappings(CorsRegistry registry) { registry.addMapping("/**") .allowedOriginPatterns("*") .allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS") .allowedHeaders("*") .allowCredentials(true); } }

这种配置在纯SpringMVC项目里没问题,但是一旦引入SpringSecurity,它的执行顺序在Security过滤器链之后。也就是说,请求先被Security拦住,如果Security不允许,MVC层的跨域配置根本没有执行机会。

2.3 方案选择:为什么我推荐在Security层做配置

我见过不少项目,Security配置类里有一堆过滤器、放行规则、认证逻辑,唯独没有http.cors()。前端联调时跨域报错,排查半天,最后发现是Security把预检请求拦截了。所以在SpringSecurity项目里,我强烈建议直接通过http.cors()开启CORS支持,并配置对应的CorsConfigurationSource。

这样做的另一个好处是:你可以在同一个地方统一管理跨域策略,而不是到处散落@CrossOrigin注解。项目变大后,微服务化之后,统一管理跨域规则会省很多事。

2.4 配置类完整代码(含注释)

@Configuration public class CorsConfig { @Bean public CorsConfigurationSource corsConfigurationSource() { CorsConfiguration config = new CorsConfiguration(); // 允许的源,生产环境建议写具体域名,不要用 * config.setAllowedOriginPatterns(Collections.singletonList("*")); // 允许的请求方法 config.setAllowedMethods(Arrays.asList("GET", "POST", "PUT", "DELETE", "OPTIONS")); // 允许的自定义请求头 config.setAllowedHeaders(Collections.singletonList("*")); // 允许携带凭证(Cookie、Authorization 头等) config.setAllowCredentials(true); // 预检请求的有效期,单位秒 config.setMaxAge(3600L); UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(); source.registerCorsConfiguration("/**", config); return source; } }

注意这里有一个细节:allowedOrigins("*")和allowCredentials(true)不能同时使用,否则会报IllegalArgumentException。解决方式是使用allowedOriginPatterns("*"),它允许在开启allowCredentials的情况下匹配所有源。

然后在Security配置类里启用:

@Configuration @EnableWebSecurity public class SecurityConfig { @Bean public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { http .cors(cors -> cors.configurationSource(corsConfigurationSource())) .csrf(csrf -> csrf.disable()) .authorizeHttpRequests(auth -> auth .requestMatchers("/auth/**").permitAll() .anyRequest().authenticated() ) .addFilterBefore(new JwtAuthenticationFilter(), UsernamePasswordAuthenticationFilter.class); return http.build(); } }

这里的cors()方法是关键,它让Spring Security在过滤器链中提前注册CORS处理逻辑。configurationSource指定了使用我们自定义的配置。

3. 结合JWT的跨域配置实战

3.1 场景描述和配置目标

一个小型的后台管理系统,前端Vue跑在localhost:8080,后端Spring Boot跑在localhost:9090,登录接口和其他业务接口都在同一个后端服务上。登录成功后后端返回JWT令牌,前端将它存储在localStorage,并在后续请求的Authorization请求头中带上。

这个场景里跨域配置需要解决以下问题:

  • 登录请求本身是跨域的,需要放行。
  • 后续请求带着Authorization头,属于非简单请求,需要支持自定义请求头。
  • 业务接口需要认证,JWT过滤器会校验请求头中的令牌。
  • 前端发起请求时先有OPTIONS预检,需要让预检请求不经过认证。

3.2 跨域配置Java类实现

跨域配置的核心是CorsConfigurationSource。这个Bean的名字很重要,Spring Security在启用http.cors()时,会默认查找名为corsConfigurationSource的Bean,如果没有找到,它会用CorsConfiguration的默认配置。所以建议把Bean方法名就叫corsConfigurationSource,省得配置指定名字时还要额外指路。

配置里有一个容易被忽略的点,就是allowedHeaders("*")。有些后端会对请求头做严格限制,如果漏了Authorization,那么浏览器预检时发现服务器不允许Authorization头,请求同样会被拦。*最省心,但是如果你有安全洁癖,可以显式列出:

config.setAllowedHeaders(Arrays.asList("Authorization", "Content-Type", "X-Requested-With"));

3.3 SecurityFilterChain集成

前面已经贴过集成方式,这里再强调几个点。

第一,CSRF在前后端分离+JWT模式下建议关闭。CSRF防护主要是防止Cookie自动携带的跨站请求,JWT放在Authorization头里其实不受CSRF影响,但是我们关闭CSRF可以省去很多不必要的麻烦。如果你坚持保留CSRF,也要注意给所有接口配置CSRF token。

第二,对预检请求要做放行处理。虽然CorsFilter会直接处理预检请求,但在authorizeHttpRequests里,还是建议显式放行OPTIONS请求:

.authorizeHttpRequests(auth -> auth .requestMatchers("/auth/login").permitAll() .requestMatchers(HttpMethod.OPTIONS, "/**").permitAll() .anyRequest().authenticated() )

上面这段代码的意思是:OPTIONS请求全部放行,不需要登录。加了这一行,哪怕CORS过滤器某天没有按预期执行,预检请求也不会被Security拦截,至少有兜底方案。

3.4 自定义JWT过滤器注意要点

如果项目里使用自定义的JWT过滤器,还要注意它和CorsFilter的执行顺序。http.addFilterBefore()这个方法用来把自定义过滤器插入到某个过滤器之前。一般JWT过滤器建议加在UsernamePasswordAuthenticationFilter之前,但一定不能早于CorsFilter。

什么情况下会出问题?假设你的JWT过滤器是这样写的:

public class JwtAuthenticationFilter extends OncePerRequestFilter { @Override protected void doFilterInternal(...) { String token = request.getHeader("Authorization"); // 如果token为空,直接返回401 if (token == null) { response.setStatus(HttpServletResponse.SC_UNAUTHORIZED); return; } // 校验token... } }

如果这个过滤器排在CorsFilter前面,预检请求到来时它没有Authorization头,于是直接返回401。前端看到的依然是跨域失败,但实际上问题出在过滤器顺序。所以我建议自定义过滤器在解析不到token时不要直接返回401,而是放行让后面的过滤器处理,或者判断一下如果请求方法是OPTIONS就直接放行。

这里贴一个兼容方案:

@Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { if ("OPTIONS".equalsIgnoreCase(request.getMethod())) { filterChain.doFilter(request, response); return; } String token = request.getHeader("Authorization"); if (token == null) { filterChain.doFilter(request, response); return; } // 正常校验token }

这样JWT过滤器不会在预检环节把请求拦死,真正的业务请求再由后续逻辑控制。

4. 常见问题与排查实录

4.1 问题一:OPTIONS请求被拦截,返回403

这个现象在SpringSecurity项目里最为常见。前端控制台显示跨域失败,点击请求详情能看到发送的是OPTIONS请求,状态码403。

排查思路:从请求在过滤器链中的流动路径入手。先看Security配置里有没有调用http.cors()。没调用的话,Spring Security根本不会内置处理CORS的过滤器。再看有没有对OPTIONS请求做permitAll处理。如果你用的是Spring Security 6,requestMatchers写法跟5不一样,注意别把antMatchers直接复制过来用。

4.2 问题二:allowCredentials(true)和allowedOrigins("*")冲突

当你在CorsConfiguration里同时设置了allowCredentials(true)和allowedOrigins("*"),启动时不会报错,但运行时一旦有跨域请求,Spring会抛出IllegalArgumentException,提示不能同时使用。

原因是:允许所有源同时允许携带凭据,这在浏览器层面是一个危险组合。解决方案就是前面提到的用allowedOriginPatterns("*")替代allowedOrigins("*"),这样Spring会动态为具体请求生成对应的Access-Control-Allow-Origin响应头。

4.3 问题三:Access-Control-Allow-Origin响应头出现多个值

这个错误的表现是浏览器报错:The 'Access-Control-Allow-Origin' header contains multiple values '*, *', but only one is allowed。

出现这个问题的典型场景是在网关层和后端服务层都配置了CORS。比如Nginx配了一遍跨域,Spring Boot里又配了一遍,两层都会往响应里添加Access-Control-Allow-Origin头,最终浏览器收到两个值,直接拒绝响应。

排查方法:看所有可能修改响应头的中间件。Nginx的add_header语法在特定层级生效,如果你在server级别加了Access-Control-Allow-Origin,同时在location级别也加了,可能会重复添加。解决方案是统一跨域配置的层级,或者在Nginx层通过proxy_hide_header隐藏上游的CORS头再统一添加。

4.4 问题四:自定义请求头导致预检失败

项目里有这样一个需求:前端在请求头里放了一个自定义的X-Client-Version头,用来做客户端版本追踪。上线后联调发现,所有请求都报跨域错误。

原因是服务器允许的请求头列表里没有X-Client-Version。预检请求发送时,Access-Control-Request-Headers会带上x-client-version,服务器返回的Access-Control-Allow-Headers里如果没有它,浏览器就会拦截。解决办法是把允许的请求头列表改为*,或者把所有自定义头都列进去。

4.5 跨域配置排查速查表

现象可能原因排查步骤
控制台报No 'Access-Control-Allow-Origin' headerSecurity层没配置cors()或CorsFilter未生效检查SecurityFilterChain中有没有cors()调用
OPTIONS请求返回403预检请求被Security拦截在authorizeHttpRequests中放行OPTIONS请求
OPTIONS请求返回401JWT过滤器在预检阶段直接返回了401自定义过滤器中对OPTIONS请求放行
Access-Control-Allow-Origin出现多个值网关、Nginx、应用有多层CORS配置逐层排查响应头叠加问题
allowCredentials和allowedOrigins冲突报错Java配置使用了不兼容组合改用allowedOriginPatterns通配
请求带Cookie但浏览器不收allowCredentials未开启设置setAllowCredentials(true)
部署到服务器后跨域失效只配置了localhost,未配置线上域名allowedOrigins或allowedOriginPatterns主动补充线上域名
预检成功但业务请求仍被拦截业务请求带了Authorization头,但JWT过滤器没放行在JWT过滤器中检查Authorization头解析逻辑

4.6 一个Nginx层的坑

有一种情况容易忽略:如果你用Nginx做反向代理,那么Nginx默认会把上游设置的Access-Control-Allow-Origin透传下去。如果Nginx自己又加了一层CORS配置,就会出现前面说的多个值。更推荐的做法是没有多网域需求时,在应用层统一处理CORS,Nginx只做反向代理和HTTPS终止,不掺和跨域配置。这样排查问题的时候会非常清晰,不用猜到底哪一层加的头。

我做过一个项目,前后端联调时一切正常,部署到测试环境后突然全部跨域报错。后来发现是因为前端静态资源也是Nginx代理的,Nginx配置里复制了一段网上的add_header Access-Control-Allow-Origin *,而上游接口也返回了CORS头,两边一叠加,浏览器直接罢工。删掉Nginx那段配置后一切恢复。

5. 最后的几点实操经验

跨域配置这件事,表面看是一堆配置代码的事,但真正让人头疼的往往是配置“位置”和“顺序”问题。SpringSecurity的过滤器链就像一个关卡重重的大楼,CORS是第一个门卫,认证是第二个门卫,授权是第三个门卫。请求进来时,门卫按顺序查验,任何一个门卫不过关,后续的业务代码都无法执行。所以你在楼里最深处做的跨域设置,如果连第一个门卫都没打点好,等于白配。

在实际开发中,我个人的习惯是:

第一,配置跨域前先确认项目里有没有SpringSecurity。有的话,直接忽略MVC层面的跨域配置,一条心把corsConfigurationSource这个Bean做好。

第二,不要迷信*。开发阶段图方便用allowedOriginPatterns("*")可以理解,但上线前一定把源换成具体的域名。安全运维看日志时,一个全通的CORS策略是很扎眼的。尤其是涉及用户隐私或财报数据的系统,CORS配置过宽和数据库裸奔没有本质区别。

第三,把预检请求放行当成默认规则。不管你的CorsFilter写得多么好,在authorizeHttpRequests里加上OPTIONS请求放行,属于多一层保险。我遇到过几次因为过滤器顺序调整导致预检被JWT过滤器拦截的情况,加了OPTIONS放行之后再也没发生过同类事故。

第四,排查跨域问题时胆子要大,直接在浏览器控制台里看响应头。打开开发者工具的Network面板,点击那条红色的请求,看响应头里有没有Access-Control-Allow-Origin,以及它的值是什么。这个头存在说明按道理后端已经放行了,这时要往前端代码找原因;这个头不存在,说明你的CORS过滤器没执行,往过滤器链的顺序和配置上找原因。这一步能帮你节省两个小时。

最后再分享一个判断跨域问题是不是Security导致的技巧:直接把Security的配置类注释掉,如果跨域立刻恢复正常,那问题就跑不出Security的过滤器链。这种二分定位法在前后端联调时特别有用。

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

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

立即咨询