ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

SpringSecurity跨域配置实战:解决CORS预检请求被拦截问题

SpringSecurity跨域配置实战:解决CORS预检请求被拦截问题 前后端分离的项目做多了总能碰到这类问题前端在浏览器里访问接口控制台报错说“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配置CorsRegistrySpringMVC本身也提供跨域配置方式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 6requestMatchers写法跟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配置就会出现前面说的多个值。更推荐的做法是没有多网域需求时在应用层统一处理CORSNginx只做反向代理和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的过滤器链。这种二分定位法在前后端联调时特别有用。
返回列表