新闻详情

新闻详情

首页 / 资讯中心 / 详情

Spring Security跨域配置实战:预检请求被拦截与CorsFilter正确用法

发布时间:2026/9/30 2:59:59来源:尧图网络
Spring Security跨域配置实战:预检请求被拦截与CorsFilter正确用法
前后端分离做了几年Spring Security 和跨域这对组合几乎是每个项目都绕不开的坎。前阵子接手一个老项目前端同事反馈所有带Token的请求在浏览器里全是红色CORS报错我打开DevTools一看清一色的OPTIONS预检请求403。当时第一反应是Controller上明明加了CrossOrigin啊结果查到最后才发现问题根本不在MVC那一层而是请求在进Controller之前就被Security过滤链拦掉了。这篇文章就专门讲讲Spring Security下的跨域处理浏览器到底在拦什么、为什么CrossOrigin在Security面前会失效、Spring Security 6.x怎么配才是正确姿势以及我实际踩过的那些坑。1. 浏览器眼中的“源”和跨域拦截规则1.1 Origin到底是什么为什么cross-origin被翻成“跨域”很多人第一次看到跨域两个字下意识以为是要跨越某个物理区域或者网络区域其实这里的域在Web语境下指的是Origin源。一个源由三部分组成协议、域名、端口。只要这三者里有任何一个不同两个地址就是不同的源。举个例子页面地址接口地址是否同源https://www.example.com:8443https://www.example.com:8443/api/user同源https://www.example.com:8443https://www.example.com:9443/api/user跨域端口不同https://www.example.com:8443http://www.example.com:8443/api/user跨域协议不同https://www.example.com:8443https://api.example.com:8443/api/user跨域域名不同浏览器在加载一个页面之后如果页面里的JS要发起请求请求的目标地址和当前页面的地址不同源浏览器就会执行同源策略Same-Origin Policy。同源策略是浏览器最基础的安全模型它保证了来自页面A的脚本不能随意读取页面B的数据。没有这个限制的话你在网上随便打开一个恶意网页这个网页里的脚本就能偷偷调你银行系统的接口、读你邮箱里的内容那整个Web世界就乱套了。CORSCross-Origin Resource Sharing跨源资源共享就是在这个安全模型上开的一个口子。服务端在响应里声明我允许哪些源来访问我浏览器拿到响应后校验这个声明校验通过才把数据交给JS校验不通过就直接报错并把响应拦截掉。所以跨域问题本质上不是请求发不出去而是响应拿不回来。请求已经到服务端了服务端也可能正常处理了但是浏览器不让JS读取结果。这个认知很重要因为很多人在排查跨域问题时总觉得是后端没收到请求实际上后端日志里可能处理得明明白白。1.2 简单请求和预检请求跨域卡顿的关键CORS请求分两类简单请求和预检请求。简单请求需要同时满足以下条件请求方法是GET、HEAD、POST其中之一请求头只能是浏览器自带的安全字段比如Accept、Content-Type等Content-Type的值不能超出application/x-www-form-urlencoded、multipart/form-data、text/plain这三种。除开简单请求之外其他请求都会先触发预检。预检是一个OPTIONS请求浏览器把它发出去目的是问服务端我接下来要发一个POST请求带着Authorization头Content-Type是application/json你允许吗服务端通过响应头里的Access-Control-Allow-Methods、Access-Control-Allow-Headers告诉浏览器允许哪些方法和头。浏览器确认没问题之后才会发起真正的业务请求。这里就有个和Spring Security直接相关的点现在前后端分离项目里身份认证信息放在Authorization请求头里或者登录之后靠Cookie传递。Authorization是自定义头所以几乎所有带认证信息的请求都会触发预检。而一旦触发预检OPPTIONS请求本身是不带认证信息的它就是一个裸的空请求。如果Spring Security的过滤链在CORS处理之前就把这个OPTIONS请求拦截了返回401或403浏览器就会认为服务端不允许跨域从而显示CORS错误。后面第2节要讲的CrossOrigin失效核心逻辑就在这。2. 引入Spring Security后CrossOrigin为什么开始“失灵”2.1 请求先去Security过滤链还是先去ControllerSpring Security在Web应用里不是一个注解它是一整套Filter链。Servlet容器收到请求后会按照过滤器顺序一层一层往下传最终才到DispatcherServlet再由DispatcherServlet分发给Controller。Spring Security的过滤链在整个链路里的位置很靠前它在请求到达MVC之前就开始做认证和授权判断了。所以问题就来了Controller方法上标注的CrossOrigin它的处理逻辑在MVC的HandlerMapping层这个层在Security过滤链的后面。当浏览器发来一个OPTIONS预检请求请求带着Origin头但是没有携带任何认证凭证Spring Security的过滤链在这个请求还没走到Controller时就直接判定未认证然后返回401/403。预检请求响应异常浏览器自然就不会放行真正的业务请求。我在项目里见过不止一次这种场面Controller上CrossOrigin写得规规矩矩单独用Postman测试接口也一切正常但前端在浏览器里就是跨域报错。因为Postman根本不走浏览器同源策略它不会发预检当然测不出来。一旦理解了请求在链路里的位置这个问题就特别好定位只要看OPTIONS请求的响应状态码是不是401/403基本就能断定是Security先拦截了。2.2 cors()与CorsConfigurationSource的关系Spring Security从很早的版本就提供了cors()配置项它的作用是在Security过滤链内部插入一个CorsFilter。这个CorsFilter会处理两件事对预检请求直接给出响应并中断后续过滤链对真实请求补上CORS响应头。这里引出一个关键配置项CorsConfigurationSource。它负责提供CORS规则规则包括允许哪些源、哪些方法、哪些请求头、是否允许携带凭证等。Spring Security在使用cors()时获取CorsConfigurationSource有三种路径显式指定在cors(customizer)里直接传入configurationSource自动查找调用cors(Customizer.withDefaults())时如果Spring容器里存在名为corsConfigurationSource的Bean会自动使用回退到MVC配置如果上面两种情况都不满足但项目里存在Spring MVC的CORS配置Spring Security会尝试借用MVC的CORS配置。我在实际项目里几乎只用第一种或第二种因为这样最可控。你要知道配置从哪里来排查时才不会迷路。回退到MVC配置这种路径我个人不太推荐在正式项目里依赖因为很容易出现两边都配了但两边都没生效或者两边都生效了响应头重复的困惑。2.3 三种配置方式的优先级对比简单整理一下常见的跨域配置方式方便你心里有个底配置方式作用位置是否会被Security拦截适用场景Controller上的CrossOrigin注解MVC层Security之后会被拦截单个接口、内部调试WebMvcConfigurer的addCorsMappingsMVC层Security之后预检可能被拦截无Security的纯MVC项目全局CorsFilter BeanServlet容器层位置不可控取决于注册顺序可以工作但需小心顺序http.cors() CorsConfigurationSourceSecurity过滤链内部在认证逻辑之前处理预检前后端分离、Security项目首选结论很直接只要项目里用了Spring Security最稳妥的方案就是走http.cors()。这不是说另外三种方式一无是处而是它们在Security环境下都或多或少存在前置拦截的隐患。WebMvcConfigurer那种写法在无Security的Spring Boot项目里很好用但和Security一起用时你得额外放行OPTIONS请求否则预检还是会卡住。3. Spring Security 6.x 跨域配置的正确姿势3.1 从零写一个CorsConfigurationSource Bean推荐的做法是显式定义一个CorsConfigurationSource Bean把CORS规则统一收拢到一处。我用的是Spring Security 6.xSpring Boot 3.x下面是核心配置import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.web.cors.CorsConfiguration; import org.springframework.web.cors.CorsConfigurationSource; import org.springframework.web.cors.UrlBasedCorsConfigurationSource; import java.util.Arrays; import java.util.Collections; Configuration public class CorsConfig { Bean public CorsConfigurationSource corsConfigurationSource() { CorsConfiguration config new CorsConfiguration(); // 允许的源。注意allowCredentials(true)时不允许用* config.setAllowedOriginPatterns(Collections.singletonList(http://localhost:*)); // 允许的请求方法 config.setAllowedMethods(Arrays.asList(GET, POST, PUT, DELETE, OPTIONS, PATCH)); // 允许的请求头 config.setAllowedHeaders(Arrays.asList(Authorization, Content-Type, X-Requested-With, Accept, Origin)); // 允许响应头中暴露给前端JS的字段 config.setExposedHeaders(Arrays.asList(Content-Disposition, Location, X-Token-Expired)); // 预检请求的有效期单位秒过期后会再次发送OPTIONS config.setMaxAge(3600L); // 是否允许携带Cookie等凭证 config.setAllowCredentials(true); UrlBasedCorsConfigurationSource source new UrlBasedCorsConfigurationSource(); // 对项目里所有接口生效 source.registerCorsConfiguration(/**, config); return source; } }这里有个细节值得多说一句setAllowCredentials(true)和setAllowedOrigins里的是互斥的。浏览器规范里明确要求允许携带凭证时Access-Control-Allow-Origin不能是通配符必须是具体的源。很多新手在这栽过跟头把allowedOrigins配成又把allowCredentials设成true结果浏览器照样拦截。解决办法就是用setAllowedOriginPatterns它支持通配符写法和Pattern匹配在allowCredentials(true)场景下是正规解法。allowedOriginPatterns的另一个好处是支持域名后缀匹配比如https://*.example.com能覆盖多个子域。如果前端有多个固定环境地址比如预发、灰度、生产各一个域名写成一个Pattern比单列allowedOrigins要省心得多。3.2 在SecurityFilterChain里挂载CORS有了CorsConfigurationSource之后接下来要在SecurityFilterChain里把它挂上。注意不用再往Servlet容器里注册CorsFilter那样反而容易造成顺序混乱。正确做法是在HttpSecurity上启用corsimport org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.security.config.Customizer; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; import org.springframework.security.config.http.SessionCreationPolicy; import org.springframework.security.web.SecurityFilterChain; Configuration EnableWebSecurity public class SecurityConfig { private final CorsConfigurationSource corsConfigurationSource; public SecurityConfig(CorsConfigurationSource corsConfigurationSource) { this.corsConfigurationSource corsConfigurationSource; } Bean public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { http // 关键启用CORS并显式指定ConfigurationSource .cors(cors - cors.configurationSource(corsConfigurationSource)) // 前后端分离项目通常关闭CSRF或者至少对无Cookie场景谨慎评估 .csrf(csrf - csrf.disable()) .sessionManagement(session - session.sessionCreationPolicy(SessionCreationPolicy.STATELESS)) .authorizeHttpRequests(auth - auth // 放行登录接口 .requestMatchers(/api/auth/login).permitAll() // 理论上CorsFilter处理预检后OPTIONS请求不会走到这里 // 但为了保险起见依然建议显式放行OPTIONS .requestMatchers(org.springframework.http.HttpMethod.OPTIONS, /**).permitAll() .anyRequest().authenticated() ) .formLogin(form - form.disable()) .httpBasic(basic - basic.disable()); return http.build(); } }.env注意这里有个容易忽略的点CorsConfigurationSource是通过构造函数注入的。如果Bean方法名不是corsConfigurationSource比如叫myCorsConfig那http.cors(cors - cors.configurationSource(myCorsConfig))照样能用。但如果你图省事在配置文件里什么都不写、只依赖http.cors(Customizer.withDefaults())自动查找那Bean的方法名就必须是corsConfigurationSource否则Spring Security找不到。还有一个细节我在authorizeHttpRequests里仍然放行了OPTIONS虽然CorsFilter处理预检时会直接短路返回OPTIONS请求正常情况下根本到不了后面的授权规则。但这条规则属于低成本保险万一你后续调整了过滤链顺序或者有人动了CorsFilter的配置至少预检请求不会被授权逻辑误伤。这个习惯帮我避免过两次线上事故值得保留。3.3 搭配JWT过滤器时的完整示例代码实际项目里跨域几乎总是和认证一起出现。现在的常规架构是前端登录拿Token后续每个请求带Authorization头后端用JWT过滤器解析Token。这种情况下CORS配置、JWT过滤器、Security过滤链三者必须配合好。给一个完整的示例import cn.hutool.core.util.StrUtil; // 仅示例实际用你项目里的工具类 import io.jsonwebtoken.Claims; import jakarta.servlet.FilterChain; import jakarta.servlet.ServletException; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.web.filter.OncePerRequestFilter; import java.io.IOException; import java.util.List; public class JwtAuthenticationFilter extends OncePerRequestFilter { Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { String token resolveToken(request); if (StrUtil.isNotBlank(token) SecurityContextHolder.getContext().getAuthentication() null) { Claims claims parseToken(token); // 省略具体解析逻辑 String username claims.getSubject(); // 从数据库或缓存加载用户信息封装成Authentication UsernamePasswordAuthenticationToken authentication new UsernamePasswordAuthenticationToken(username, null, List.of()); SecurityContextHolder.getContext().setAuthentication(authentication); } filterChain.doFilter(request, response); } private String resolveToken(HttpServletRequest request) { String bearer request.getHeader(Authorization); if (StrUtil.isNotBlank(bearer) bearer.startsWith(Bearer )) { return bearer.substring(7); } return null; } private Claims parseToken(String token) { // 用你的JWT库解析这里省略 return null; } }然后在SecurityConfig里追加JWT过滤器http .cors(cors - cors.configurationSource(corsConfigurationSource)) .csrf(csrf - csrf.disable()) .sessionManagement(session - session.sessionCreationPolicy(SessionCreationPolicy.STATELESS)) .authorizeHttpRequests(auth - auth .requestMatchers(/api/auth/login).permitAll() .requestMatchers(HttpMethod.OPTIONS, /**).permitAll() .anyRequest().authenticated() ) .addFilterBefore(jwtAuthenticationFilter(), UsernamePasswordAuthenticationFilter.class);关于这个JWT过滤器和跨域的关系我多说几句。有人习惯在JwtAuthenticationFilter里判断如果请求方法是OPTIONS就直接放行这个做法在CorsFilter已经生效的情况下其实多余。因为OncePerRequestFilter的执行位置在Security过滤链内部而CorsFilter在更早的位置预检请求早就被CorsFilter短路回给浏览器了根本轮不到JWT过滤器执行。但如果你的CorsFilter因为某些原因没生效JWT过滤器里加这个判断确实能兜底所以我也不反对这种双保险做法。真正要注意的是不要在JWT过滤器里对OPTIONS请求做特殊处理之后又忘记把响应头写全否则就算放行了浏览器也拿不到必要的CORS响应头照样跨域失败。4. 实际项目中的常见坑和排查实录4.1 OPTIONS预检被拦截403/401的经典现场这是我见过最多的情况症状是浏览器Network面板里能看到两个请求第一个是OPTIONS状态码401或403第二个真正的POST请求直接显示CORS error甚至都不发出去。原因基本就是我前面说的链路顺序问题。排查步骤可以这样走先用curl手动发一个OPTIONS请求直接看后端返回什么。比如curl -i -X OPTIONS http://localhost:8080/api/user/list \ -H Origin: http://localhost:5173 \ -H Access-Control-Request-Method: POST \ -H Access-Control-Request-Headers: Authorization正常预期返回200并且响应头里有Access-Control-Allow-Origin和Access-Control-Allow-Methods。如果返回401/403说明预检请求在进入CorsFilter之前就被认证机制拦了或者CorsFilter压根没配置上。这时候按第3节的方案重新配置http.cors()问题基本就解决了。还有一种情况比较隐蔽OPTIONS返回200了响应头也有Access-Control-Allow-Origin但前端还是报跨域。这时候要检查响应头里的Access-Control-Allow-Headers是否包含了前端实际要带的请求头。比如前端发了Authorization和X-Custom-Header两个头服务端只允许了Authorization浏览器校验Access-Control-Request-Headers时发现X-Custom-Header不在允许列表里一样拦截。这种问题光看响应里有没有CORS头是不够的要逐个核对方法、请求头、源三个维度。4.2 带Cookie请求失败allowCredentials与通配符的冲突如果项目采用Cookie做会话凭证也就是登录后Set-Cookie后续请求自动携带Cookie那CORS配置里必须把setAllowCredentials(true)打开同时前端发起请求时要设置withCredentials: trueaxios里是withCredentialsfetch里是credentials: include。这个场景最经典的坑就是后端配置写了config.setAllowedOrigins(Collections.singletonList())又写了config.setAllowCredentials(true)。这在Spring里不会报配置错误但是浏览器校验响应头时会发现两个要求矛盾Access-Control-Allow-Origin是但Access-Control-Allow-Credentials是true。正规的浏览器直接拒绝这种组合报错信息通常引用CORS规范。解决办法很简单改成setAllowedOriginPatterns并写明具体的源前缀或者用allowedOrigins里枚举所有前端地址。顺带提一句调试技巧Chrome本地开发时临时跨域测试可以开一个禁用了Web安全的浏览器实例用命令行加参数启动。但这只是本地开发调试的手段千万别让这个习惯进入生产依赖也不要用它替代真正的CORS配置。生产环境老老实实把allowedOriginPatterns配好该加的响应头一个都不能少。4.3 Nginx、多前端环境下的配置分工项目上线时前端静态资源和后端API往往不在同一个源。很多团队会选择用Nginx做前端静态服务器的反向代理把/api路径的请求转发到后端服务。这种情况下有两条路一是Nginx层面处理跨域在location块里加上Access-Control-Allow-Origin等响应头并且对OPTIONS请求直接返回204不往后端转发。这种方案的优点是后端不感知跨域Java代码里甚至可以不配CORS。缺点是Nginx配置属于运维层前端如果后续新增了自定义请求头需要运维同步改Nginx配置沟通链路变长。二是后端处理跨域Nginx只做转发把请求原样透传给后端。这种方案下CORS响应头由Spring Security的CorsFilter统一生成规则都在代码里环境不同改配置即可。我个人的偏好是第二种因为跨域规则本质上是业务接口对外开放策略的一部分放在代码里用Git管理比散落在不同服务器的Nginx配置里更可控。这里要特别提醒一个Nginx透传场景下的坑Nginx默认会忽略客户端请求里的某些头比如Authorization一般是保留的但如果你自定义了一个X-Auth-Token之类的头Nginx默认可能不转发导致后端拿不到这个头认证失败。这时候需要在Nginx配置里用underscores_in_headers或显式配置proxy_set_header带上对应头。如果你发现帮我查一下为什么后端拿不到某个自定义头特别像跨域问题但CORS配置又没问题先查一层Nginx。4.4 排查工具与快捷定位方法跨域问题排查我一般按下面这个顺序来基本三分钟内能定位到八九成先看浏览器Network面板里请求是被拦截还是没收到响应。被拦截的表现是请求状态能正常返回但console里报CORS error这种基本是响应头不匹配没收到响应或者前端直接报network error再去查服务端有没有收到这个请求。如果服务端没收到预检请求说明请求在更外围被拦了可能是Nginx、网关也可能是浏览器本身压根没发先查Nginx日志和access log。如果服务端收到了OPTIONS但返回非200重点查Security过滤链和CorsFilter的配置。如果OPTIONS返回200且带了CORS头但真实请求还是报错把curl带上完整的请求头复现一遍对比浏览器发出的请求头和服务端允许的请求头。另外Chrome DevTools的Network面板里CORS报错请求那一行的Preview或Response标签页往往能直接看到服务端返回的错误信息有时候服务端返回的是JSON错误体只是被浏览器拦住了。千万别只看console里的CORS error五个字就觉得是无解问题方法总比问题多只是被拦住了而已。我还习惯用curl写一个小脚本把浏览器预检请求完整模拟一遍脚本里带上Origin、Access-Control-Request-Method、Access-Control-Request-Headers三个头然后直接看响应。能快速区分是Security的问题还是MVC配置的问题比浏览器里反复刷新调试省事得多。最后再分享一个我自己的配置习惯CORS规则的粒度不要一刀切。登录、注册、验证码这类接口可以放得松一点业务接口按环境控制allowedOriginPatterns管理后台接口单独限制更严格的源。Spring Security的http.cors()配的是全局规则如果要做精细控制可以在CorsConfigurationSource里按路径注册不同的规则比如registerCorsConfiguration(/api/admin/, adminCorsConfig)和registerCorsConfiguration(/api/web/, webCorsConfig)分开配。跨域不是能通就行它同样是接口暴露面的一部分配得越精确出问题的时候越好查。这个项目后来我把所有跨域配置统一收口到CorsConfig类里Bean名字固定为corsConfigurationSourceSecurityConfig里只写一行http.cors(cors - cors.configurationSource(corsConfigurationSource))。前端新增环境、新增自定义请求头都只改这一个文件上线两年再没出现过跨域的线上事故。如果你也在为Spring Security的跨域问题挠头照着这个思路配一遍绝大多数问题都能迎刃而解。
网站建设高端定制企业官网
RELATED

相关资讯

更多精彩内容,欢迎继续阅读

较早相关资讯

最新相关资讯

模型优化实战:量化、剪枝与混合精度如何平衡推理速度与精度 2026/9/30 3:58:32

模型优化实战:量化、剪枝与混合精度如何平衡推理速度与精度

"Model-Optimizer"这个词,我是在一次模型上线被逼到墙角的时候才真正理解的。当时模型在验证集上F1接近85,我信心满满地开始量化部署,结果INT8一跑,精度直接掉到72,紧接着剪枝又砍到79,前前后后折…

阅读更多 →
BFD双向转发检测原理与实战:毫秒级链路故障感知 2026/9/30 3:58:32

BFD双向转发检测原理与实战:毫秒级链路故障感知

简介:本资源是一份面向网络工程师、运维人员及通信专业学习者的BFD(双向转发检测)技术权威白皮书,聚焦解决传统协议故障检测慢(秒级)、无法满足高可用业务需求的核心痛点,适用于路由协议优化、快…

阅读更多 →
Model-Optimizer:模型部署全链路决策框架实战指南 2026/9/30 3:58:32

Model-Optimizer:模型部署全链路决策框架实战指南

1. “Model-Optimizer”不是工具名,而是工程目标的精准表达很多人第一次看到“Model-Optimizer”这个标题,下意识会以为它是一个现成的开源项目、某个厂商发布的GUI软件,或者像TensorRT那样带安装包的SDK。我刚接触这个概念时也这么想——直到…

阅读更多 →
大模型推理优化实战:从PT到生产服务的全栈工程指南 2026/9/30 3:58:32

大模型推理优化实战:从PT到生产服务的全栈工程指南

1. 项目概述:Model-Optimizer 不是工具名,而是一类工程实践的统称“Model-Optimizer”这个标题乍看像某个开源项目或商业软件的代号,但结合当前全网搜索热词——TensorRT、vLLM、NVIDIA驱动安装、Docker镜像拉取、PT转TRT、Qwen3-Embedding加…

阅读更多 →
大模型推理优化实战:从PyTorch到TensorRT/vLLM的端到端压榨路径 2026/9/30 3:58:31

大模型推理优化实战:从PyTorch到TensorRT/vLLM的端到端压榨路径

1. 项目概述:Model-Optimizer不是工具名,而是工程范式的代号“Model-Optimizer”这个标题乍看像某个开源工具的代号,但结合NVIDIA、TensorRT-LLM、vLLM、PT文件转换TensorRT等热搜词,它实际指向一个在AI推理落地现场反复被高频提及…

阅读更多 →
Hindsight:浏览器历史取证与痕迹分析实战指南 2026/9/30 3:58:25

Hindsight:浏览器历史取证与痕迹分析实战指南

做数字取证的朋友应该都有这个共识:拿到一台待分析的机器,第一件事往往不是翻日志、抽内存,而是先把浏览器历史拉出来。而说到解析浏览器历史,Hindsight 是我这几年用得最顺手的工具,没有之一。它能把 Chrome 和 Firef…

阅读更多 →

今日资讯

本周资讯

本月资讯

看完文章仍有疑问?

联系尧图顾问,获取一对一建站咨询

立即免费咨询 📞 400-888-8888
📞 ✉