ARTICLE DETAIL

资讯详情

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

Spring中CorsFilter配置全指南:原理、实战与排坑

Spring中CorsFilter配置全指南:原理、实战与排坑 前后端联调的时候浏览器控制台突然冒出一整屏红色的报错像这样Access to fetch at http://api.localhost:8080/user/info from origin http://localhost:3000 has been blocked by CORS policy: No Access-Control-Allow-Origin header is present on the requested resource.第一反应是先上网搜错误码搜出来的帖子十个里有八个让加个CrossOrigin注解另外两个让配CorsFilter。然后照着抄了一通重启服务发现有的接口好了有的接口还在报有的请求通了带Authorization头的请求又断了。最后折腾半天才发现问题根本不在“有没有配CORS”而在“Filter到底放在了哪一层、配置类到底有没有被加载”。这篇文章只讲一件事Spring里用Filter方式解决跨域到底该怎么写、怎么配、怎么排坑。我在项目里从Spring MVC一路用到Spring BootFilter也手动注册过、通过OncePerRequestFilter继承过、跟Spring Security打过架踩过的坑比文档里写的内容多得多。这篇东西没有废话全是可直接上手的配置、原理和排查经验。1. 跨域报错的根源同源策略与预检请求1.1 为什么浏览器会拦你的请求跨域的全称叫“跨源资源共享”英文是Cross-Origin Resource Sharing缩写CORS。这个机制说的是浏览器在发起请求时会检查当前页面所在的源协议域名端口和请求目标地址的源是否一致。如果两者不一致就叫跨域。这里的关键在于跨域限制是浏览器的行为不是服务器的行为。你的Spring后端其实已经把数据返回了浏览器只是收到了响应之后发现响应头里没有Access-Control-Allow-Origin于是自行拦截不让JavaScript读取。所以很多新手会奇怪明明F12里能看到网络请求都是200为什么前端就拿到不到数据正是因为拦截发生在这两个环节之间。我见过的最常见误解是把跨域问题当成“后端没返回数据”。实际上用curl或Postman去访问一切正常只有浏览器环境里才会报错。这说明服务端逻辑没问题缺的是让浏览器放行的那几个响应头。哪些请求会被浏览器特殊对待简单说有三个特征GET/HEAD/POST这三种方法、自定义Header不能随便加、Content-Type只能是text/plain、multipart/form-data或application/x-www-form-urlencoded。只要超出这个范围浏览器就会先发一个OPTIONS预检请求Preflight试探服务器是否允许真实请求。这也是为什么很多接口在Swagger或Postman里调试没问题一接前端就出状况。1.2 Filter、Interceptor、注解三条路线怎么选Spring里解决跨域有几种手段CrossOrigin注解、WebMvcConfigurer里重写addCorsMappings方法、实现OncePerRequestFilter以及直接注册CorsFilter。CrossOrigin注解最轻量适合单接口快速调试。但缺点很明显每个Controller都要加接口一多就漏。addCorsMappings是Spring MVC层面的全局配置对Controller接口生效但如果请求在进入DispatcherServlet之前就被Filter拦截比如Spring Security这个配置就完全没用。Filter方式是最底层的因为Filter在Servlet容器里就执行了理论上能覆盖到所有请求所以也是最稳妥的选择。项目的演进路径通常是这样的初期用CrossOrigin快速验证后来接口多了改addCorsMappings再后来接入了Spring Security或者自定义了登录鉴权Filter发现配置失效最终转向CorsFilter这条路。这篇文章讲的CorsFilter指的就是第三种方案也是我最终在多个项目里沉淀下来的稳定做法。2. CorsFilter的核心原理一次请求在其中经历了什么2.1 从注册到生效CorsFilter的两种装配方式Spring里实现CORS Filter的入口是org.springframework.web.filter.CorsFilter。这个类继承自OncePerRequestFilter保证了每次请求时只执行一次过滤逻辑避免多次包含过滤链导致的重复执行。使用它之前需要准备一个CorsConfigurationSource通常用UrlBasedCorsConfigurationSource实现。第一种方式是基于Java配置的显式注册。如果项目是Spring Boot可以专门定义一个配置类Configuration public class CorsConfig { Bean public CorsFilter corsFilter() { CorsConfiguration config new CorsConfiguration(); config.addAllowedOrigin(http://localhost:3000); config.addAllowedMethod(*); config.addAllowedHeader(*); config.setAllowCredentials(true); config.setMaxAge(3600L); UrlBasedCorsConfigurationSource source new UrlBasedCorsConfigurationSource(); source.registerCorsConfiguration(/**, config); return new CorsFilter(source); } }第二种方式是在Spring Boot的配置文件里直接声明。Spring Boot对CORS有相对完善的自动配置只用这些属性就能覆盖多数场景spring.web.cors.allowed-originshttp://localhost:3000 spring.web.cors.allowed-methodsGET,POST,PUT,DELETE,OPTIONS spring.web.cors.allowed-headers* spring.web.cors.allow-credentialstrue spring.web.cors.max-age3600不过application.properties里的这些配置最终也还是要通过CorsRegistration转成CorsConfigurationSource再由内部的CorsFilter来生效。如果你不需要精细到路径级别的控制这种方式省事需要分路径定制策略时还得回到Java配置。2.2 请求匹配与包装CorsFilter内部是怎么判断和响应预检的CorsFilter的doFilterInternal方法里做的事情可以简单拆成三步。第一步通过CorsProcessor去拿请求里的Origin头。如果请求里没有Origin说明不是跨域请求CorsFilter会直接放行不添加任何CORS相关头。第二步按请求路径从CorsConfigurationSource里匹配对应的CorsConfiguration对象。比如你注册了/api/**的配置那/api/user/list能命中/static/js/app.js不会命中。这里要注意的是路径匹配用的是Spring的AntPathMatcher语法/**才是匹配所有路径的写法。很多人只配了/*结果只匹配到一级路径二三级路径直接失效——这是特别蠢又特别隐蔽的一个坑。第三步检查请求方法。如果是OPTIONS请求且带有Access-Control-Request-Method头就认定为预检请求直接调用preflightResponse处理。如果预检通过由它返回200的响应并写上那些允许跨域的响应头如果预检不通过直接返回403根本不会进入Controller。如果是普通GET/POST请求就把CorsConfiguration里定义的允许跨域头复制到响应头上。CorsFilter处理完之后响应头里会出现这几个关键HeaderAccess-Control-Allow-Origin允许的来源Access-Control-Allow-Methods允许的方法Access-Control-Allow-Headers允许的自定义请求头Access-Control-Expose-Headers允许前端JavaScript读取的响应头Access-Control-Max-Age预检结果缓存时间单位秒理解了请求匹配机制很多灵异问题都能解释通。比如为什么Filter配置了/api/**根路径的接口还是报CORS错误因为压根没走到Filter的匹配分支里。3. 配置项逐一拆解每个参数背后的真实含义3.1 allowedOrigins与“*”的误区Credentials才是罪魁祸首这是CORS配置里最大的坑我这里单独拿出来说。很多人图省事直接把allowedOrigins设置成*表示允许所有来源访问。这个写法在部分场景下确实能用但一旦跟allowCredentials(true)搭配立刻失效。因为浏览器的规范明确要求使用Credentials时Access-Control-Allow-Origin不能是通配符*。服务器如果这样返回浏览器直接拒绝响应。什么叫Credentials简单说就是请求带着Cookie或者Authorization头这种凭证信息。前端Axios里设置withCredentials: true或者带了Authorization: Bearer xxx请求头都属于这类。而这恰恰是现代前后端分离项目最常见的请求方式。所以在生产项目里我建议不要用*而是明确列出允许的来源或者动态从配置项里读取config.addAllowedOrigin(https://admin.example.com); config.addAllowedOrigin(https://app.example.com);如果确实有多个环境本地、测试、预发共用一套代码就把允许的来源放进环境配置文件里用Value注入。这样既不会踩*的坑也不会在环境切换时手忙脚乱。这里还有个容易被忽略的细节有些浏览器对带端口和不带端口的源是分别识别的http://localhost:3000和http://localhost是两个完全不同的源都要显式声明。3.2 allowedMethods、allowedHeaders与exposedHeadersallowedMethods决定了允许哪些HTTP方法跨域。一般设置成*或列出GET,POST,PUT,DELETE,OPTIONS都行。但注意一个细节如果前端请求方法不在列表里预检请求就会直接失败。有些老项目只用POST和GETsafe起见可以设置*反正不会被允许之外的方法命中。allowedHeaders是另一个高频踩坑点。前端如果自己加了X-Token、X-Requested-With之类的自定义请求头这个配置里必须包含对应头名否则预检请求直接挂掉。设置成*能省事但配合allowCredentials(true)时同样存在不能使用通配符的限制。稳妥的做法是列全除了常用的Content-Type,Authorization之外把自己项目里自定义的头都写进去。exposedHeaders和allowedHeaders是两个方向的东西。allowedHeaders控制浏览器能带哪些请求头exposedHeaders控制前端JavaScript能读取到哪些响应头。默认情况下前端脚本只能读取Cache-Control、Content-Language、Content-Type、Expires、Last-Modified、Pragma这几个标准响应头。如果后端自定义了X-Total-Count这种业务响应头不配置exposedHeaders前端用response.headers[X-Total-Count]拿到的一定是undefined。我遇到过一个真实场景前端做表格分页数据总数放在响应头X-Total-Count里调试了半天取不到值最后才发现是exposedHeaders没配置。这个点特别不容易排查因为接口返回正常、网络面板里肉眼也能看到那个响应头但JavaScript就是读不到。3.3 maxAge有百利而无一害的配置maxAge指定的是预检请求结果在浏览器端能缓存多少秒。单位是秒比如设置3600就是一小时。为什么不设置就会拖慢请求因为每次跨域请求发出前如果预检结果没有过期浏览器不会重新发起OPTIONS请求直接跳过预检一旦没有缓存每个带自定义头的POST请求都会先来一次OPTIONS接口延迟肉眼可见地增加网络请求量也直接翻倍。生产环境里建议设置一个合理的缓存时间比如600秒或3600秒。它在绝大多数情况下只有好处没有坏处除了偶尔改了CORS配置后预检结果不即时生效——清理浏览器缓存或者在DevTools里勾选Disable cache就能解决。4. 实战配置多场景标准版、容器版、Security版4.1 标准Spring Boot集成方案把第一节里那套Java配置补全成可落地的版本需要注意的点都写在注释里Configuration public class CorsConfig { Value(${cors.allowed-origins:http://localhost:3000}) private String[] allowedOrigins; Bean public CorsFilter corsFilter() { CorsConfiguration config new CorsConfiguration(); // 显式声明来源而不是使用通配符 for (String origin : allowedOrigins) { config.addAllowedOrigin(origin); } config.addAllowedMethod(*); config.addAllowedHeader(*); // 允许携带凭证Cookie / Authorization config.setAllowCredentials(true); config.setExposedHeaders(Arrays.asList(X-Total-Count, Content-Disposition)); config.setMaxAge(3600L); UrlBasedCorsConfigurationSource source new UrlBasedCorsConfigurationSource(); source.registerCorsConfiguration(/**, config); return new CorsFilter(source); } }在application.yml里维护允许的来源列表cors: allowed-origins: - http://localhost:3000 - https://admin.example.com这套配置能解决90%以上的场景。只要Filter被Spring管理到就生效不用在每个Controller上加注解也不依赖Spring MVC的路由匹配。4.2 传统Spring MVC下的Filter注册方式非Spring Boot项目或者Servlet容器中手动部署的WAR包项目需要在web.xml或JavaConfig里声明Filter。因为CorsFilter本身是普通的javax.servlet.Filter所以装配方式和自定义Filter一样。JavaConfig版本的写法public class WebInitializer implements WebApplicationInitializer { Override public void onStartup(ServletContext servletContext) { CorsConfiguration config new CorsConfiguration(); config.addAllowedOrigin(http://localhost:3000); config.addAllowedMethod(*); config.addAllowedHeader(*); UrlBasedCorsConfigurationSource source new UrlBasedCorsConfigurationSource(); source.registerCorsConfiguration(/**, config); FilterRegistration.Dynamic corsFilter servletContext.addFilter(corsFilter, new CorsFilter(source)); corsFilter.addMappingForUrlPatterns(EnumSet.allOf(DispatcherType.class), false, /*); } }web.xml版本就是在web.xml里加filter和filter-mapping声明。核心思路一样就是把CorsFilter注册到所有URL上并确保它排在其它可能返回响应的Filter前面。4.3 与Spring Security共存顺序决定生死如果你的项目里有Spring Security那么CORS配置失效的概率极高。网络上大量帖子提到“Spring Security里CORS不生效”原因普遍出在过滤链顺序上。Spring Security的过滤器链会先于DispatcherServlet工作。如果CorsFilter没有在Security的HttpSecurity配置里显式声明那么很多被Security拦截的请求根本走不到自定义的CorsFilter。更麻烦的是Security自己处理OPTIONS预检的方式比较特殊默认情况下预检请求会被直接拒绝因为你没有给它放行的规则。正确做法是给Security开启CORS支持并注册CorsFilterConfiguration EnableWebSecurity public class SecurityConfig { private final CorsFilter corsFilter; public SecurityConfig(CorsFilter corsFilter) { this.corsFilter corsFilter; } Bean public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { http // 启用Spring Security的CORS支持它会在过滤器链中插入CorsFilter .cors(Customizer.withDefaults()) .csrf(csrf - csrf.disable()) .authorizeHttpRequests(auth - auth .requestMatchers(HttpMethod.OPTIONS, /**).permitAll() .anyRequest().authenticated() ) // 手动把CorsFilter添加到Security过滤器链中确保它在认证逻辑之前执行 .addFilterBefore(corsFilter, UsernamePasswordAuthenticationFilter.class); return http.build(); } }这里有两个动作缺一不可。第一调用.cors(Customizer.withDefaults())让Spring Security知道这个应用允许CORS第二手动addFilterBefore把自定义的CorsFilter放到认证Filter之前保证预检请求在进入认证逻辑之前就被处理掉。另外再给OPTIONS请求放行权限否则即使CORS头正确Security的鉴权也会阻止预检请求。如果你完全依赖Spring Security也可以不自己定义CorsFilter而是在HttpSecurity里通过CorsConfigurationSource配置直接构造。不过这种写法把两种方案耦合在一起排错时不容易分清问题出在哪一层。我的习惯是统一使用独立的CorsFilter作为CORS的唯一入口Spring Security里只做“放行”和“顺序”的控制。这样CORS相关逻辑集中在一个类里后续改配置、查问题都方便得多。5. 踩坑实录几个真实项目的排错过程5.1 加了Filter还是报错先查Filter到底有没有被加载有一位同事在自己的Spring Boot项目里按示例抄了CorsConfig测试环境一切正常部署到某一台机器上后死活报跨域错误。第一反应是代码没有发上去对比之后发现分支一致、构建时间一致、产物一致但就是不行。最后排查到那里是内网网关层做了请求转发同时网关自己处理了OPTIONS请求。请求到网关就被截住了根本没有转发到后端应用所以无论后端CORS Filter写得多么完美都不会有对应的响应头。解决方式是在网关层统一加上CORS头。这个案例的教训是跨域报错不一定是后端代码的问题链路上任何一层都可能拦截。排错时要先确认请求确实到达了目标服务再看后端返回的响应头里有没有Access-Control-Allow-Origin。可以写一个简单的临时Controller打印所有请求头和请求URI然后看访问日志里有没有对应记录。另一个更常见的原因是Filter没有生效。Spring Boot组件扫描默认扫描的是启动类所在包及其子包如果CorsConfig放在了和启动类不同的包路径下配置类静默失效连日志都没有。所以我一般建议把CORS配置类放在config子包下并且启动类最好在根包上。5.2 allowCredentials和allowedOrigins*的组合病这是最经典的CORS配置组合冲突。很多教程里给出的基础示例就是addAllowedOrigin(*)setAllowCredentials(true)让人误以为这是标准搭配。实际运行起来浏览器会抛出这样的错误The value of the Access-Control-Allow-Origin header in the response must not be the wildcard * when the requests credentials mode is include.这个报错出来的时候后端其实已经正常返回了200和响应头浏览器仍然拒绝前端读取。我排查过几个项目最终发现都是这种组合。解决方式也不复杂显式列出允许的来源替代通配符。还有一种相关但更隐蔽的场景当来源非常多根本没法穷举时又必须要配合Credentials使用。这时可以自定义CorsConfigurationSource在getCorsConfiguration方法里动态读取请求里的Origin值把它动态添加到配置里。这种做法在某些需要支持任意来源但还要带Cookie的场景下特别实用Component public class DynamicCorsConfigurationSource implements CorsConfigurationSource { Override public CorsConfiguration getCorsConfiguration(HttpServletRequest request) { String origin request.getHeader(Origin); CorsConfiguration config new CorsConfiguration(); if (StringUtils.hasText(origin)) { config.addAllowedOrigin(origin); } config.addAllowedMethod(*); config.addAllowedHeader(*); config.setAllowCredentials(true); config.setMaxAge(3600L); return config; } }动态来源方案的缺点是安全性有所降低任何来源都能通过建议只在不需要严格校验来源的接口上使用。5.3 同一请求里出现两个不同的Allow-Origin响应头有一次在浏览器控制台看到“The Access-Control-Allow-Origin header contains multiple values”的报错。这个报错的字面意思很直观响应头里出现了不止一个Access-Control-Allow-Origin比如一个值是具体域名另一个是*浏览器就懵了。原因通常是项目里同时存在两套CORS配置比如Spring Security的.cors()启用了一次CorsFilter自己又注册了一个全局CorsFilter两者都往响应里写了CORS头。于是同一个请求被两个Filter各处理了一遍响应头自然多出来一份。排查方法是在响应里直接看到底有几个Access-Control-Allow-Origin。如果看到多个要么去掉自定义的CorsFilter注册要么在Security里不要启用.cors()二选一。我这里推荐把CORS统一托管到一个Filter里然后重复代码检查一下。5.4 重定向请求的跨域302之后头被“吃掉”这个更冷门一些但真实存在于文件下载、登录跳转这类场景里。后端接口返回302重定向浏览器跨域请求跟随重定向时CORS头在某些情况下会被丢弃导致前端还是报“no Access-Control-Allow-Origin”。原因是重定向后的响应可能来自另一个端点的另一个路径那个路径上若没有CORS配置返回里自然没有对应头。要么给重定向目标Path也注册CORS规则要么用代理方式解决前端请求同源的一个接口由后端代理转发到目标地址。这种问题特别难排查因为你看F12网络面板请求列表里有一条302和一条最终的GET两者的响应头完全不同而浏览器报错对应的是最终那条没有CORS头的响应。一旦意识到这点解决思路就清晰了。6. 生产环境里的CORS治理思路6.1 开发、测试、生产环境的配置分层CORS配置在不同环境里应该有不同的策略。开发环境来源通常只有localhost:3000测试环境可能有多个测试域名生产环境则需要严格限制。我习惯的做法是把允许的来源做成配置项而不是硬编码在代码里。Value注入的方式就行也可以用Spring的ConfigurationProperties管理一组来源。这样打包时通过Profile区分上线时只需改配置不重新编译。ConfigurationProperties(prefix cors) public class CorsProperties { private ListString allowedOrigins new ArrayList(); private ListString allowedMethods new ArrayList(); private ListString allowedHeaders new ArrayList(); private boolean allowCredentials true; private long maxAge 3600L; // getter / setter }6.2 结合网关统一解决跨域如果你的架构有网关层Spring Cloud Gateway、Nginx、Kong跨域问题还可以在网关层面统一解决。网关里配置CORS头转发给后端服务时再清理掉后端返回的CORS头保证整个请求链路上只有一层CORS处理逻辑。以Nginx为例简单配置如下location /api/ { if ($request_method OPTIONS) { add_header Access-Control-Allow-Origin https://admin.example.com; add_header Access-Control-Allow-Methods GET, POST, PUT, DELETE, OPTIONS; add_header Access-Control-Allow-Headers Authorization, Content-Type; add_header Access-Control-Max-Age 3600; return 204; } add_header Access-Control-Allow-Origin https://admin.example.com; proxy_pass http://backend; }网关层统一解决的好处是后端各服务不用重复配置但要注意预检请求默认不会带着Authorization头往下游传递很多网关配置就漏在了这一层。反正每条链路只保留一个CORS生产者这是核心原则。6.3 到底该用Filter还是WebMvcConfigurer很多人会纠结这个选择。简单归纳对比项CorsFilterWebMvcConfigurer生效层次最底层无需经过Spring MVCSpring MVC层依赖DispatcherServlet覆盖范围Filter链能到达的全部路径Controller映射路径与Spring Security先后可自行控制Filter顺序在Security之后执行预检会被Security先处理配置复杂度需要注册Bean和Source配置简单重写方法即可适合场景所有项目尤其涉及Security和网关纯API应用不涉及额外Filter链只要项目里存在Spring Security、自定义Filter、网关转发这些环节我优先推荐CorsFilter。没有这些因素、纯接口对外提供时WebMvcConfigurer更清爽。7. 最后分享几个经验和调试技巧7.1 快速复现和验证CORS请求调试CORS问题时光看浏览器报错往往不够。一个高效的办法是直接用curl手动模拟预检请求curl -i -X OPTIONS http://localhost:8080/api/user/list \ -H Origin: http://localhost:3000 \ -H Access-Control-Request-Method: GET \ -H Access-Control-Request-Headers: Authorization,Content-Type看响应头里是否有Access-Control-Allow-Origin、Access-Control-Allow-Methods、Access-Control-Allow-Headers。这个方法能很直观地判断服务端CORS配置到底生效了没有可以迅速定位问题在哪一层。如果curl模拟时有这些头而浏览器报错问题大概率在网关或者代理层如果curl都没有问题就在后端配置上。7.2 避免未来的自己踩坑的几个习惯第一CORS配置类单独放一个类不要塞到某个Controller里。第二过滤器的注册顺序不要依赖默认顺序Spring Boot多Filter共存时Order注解或者FilterRegistrationBean.setOrder()显式指定数值小的先执行。第三启动日志里偶尔可以看到Filter的注册路径用logging.level.org.springframework.webDEBUG能帮助确认Filter是否被加载。第四前端的withCredentials必须和后端的allowCredentials(true)保持一致任何一端没有声明都会导致Cookie或Authorization无法跨域携带。第五尽量避免“复读机式”地查CSDN博客新版本的Spring Boot2.7和Spring Security5.8里API有变动老帖子里的写法很可能已经过时。看官方文档的CORS章节永远是最准确的信息来源。我自己的项目到今天都是靠CorsFilter这套配置撑着的期间经历过浏览器升级、Spring Boot版本从2.x跨到3.x、前端框架从Vue2换到Vue3CORS这块代码基本没动过。稳定大于炫技配置简单但覆盖全面的方案往往才是生产环境里最省心的选择。
返回列表