ARTICLE DETAIL

资讯详情

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

@CrossOrigin配置不生效?Spring Boot微服务跨域问题排查与解决

@CrossOrigin配置不生效?Spring Boot微服务跨域问题排查与解决 最近一个群里有人问了个特别典型的问题Spring Boot 微服务中控制器上明明写了CrossOrigin(origins *)前端 Vue 项目请求过来还是报跨域而且后端不报任何异常。这种问题我在实际项目里踩过不止一次看着是注解配置缺失或者写错的小事实际上牵扯到整个请求链路里各个环节对 HTTP 头部的处理。如果你也在微服务架构里遇到过类似情况这篇内容基本能帮你把问题收敛到具体某个层面。先说结论CrossOrigin(origins *)这种写法本身并没有错但它不是万能的。尤其在微服务场景下前面挂了网关、加了 Spring Security、或者自定义了 Filter任何一个环节拦截了 OPTIONS 预检请求或者覆盖已经设置好的 CORS 响应头前端照样跨域。这不是注解没生效而是它生成的头部根本没机会到达浏览器。1. 跨域问题的本质以及为什么配置了不等于生效了1.1 浏览器同源策略到底拦的是什么前端跨域问题从根上讲是浏览器在搞事情不是后端拒绝响应。浏览器要求当前页面地址的协议、域名、端口三者都和请求目标地址一致才允许页面里的 JavaScript 读取响应内容。任意一项不同就属于跨域请求。但要注意一个细节跨域请求在大多数情况下其实已经发出去了后端也处理了只是浏览器在拿到响应之后检查响应头里有没有Access-Control-Allow-Origin这个字段并且该字段的值是否和当前页面域名匹配。如果不匹配或者根本没有这个字段浏览器就会把响应丢进黑名单前端 JavaScript 拿到的是网络层面的报错看起来就像请求失败了一样。所以在排查这类问题的时候第一件事就是要先看响应头里有没有 CORS 相关的字段不要一上来就怀疑后端没处理。1.2 简单请求与预检请求的两个阶段跨域请求又分简单请求和非简单请求。简单请求一般指 GET、HEAD、POST 这三种并且请求头只包含浏览器默认允许的字段比如 Accept、Content-Type 的三种值之一。简单请求不会触发预检浏览器直接发真实请求。一旦请求方法为 PUT、DELETE或者 Content-Type 为application/json或者请求头里带了自定义字段如X-Token、Authorization浏览器就会先发一个 OPTIONS 请求做预检。预检通过之后才发真实请求。问题就出在这里。很多后端同学只给真实请求的接口配置了CrossOrigin却忽略了 OPTIONS 预检请求也走的是同一个 Controller 映射。如果这个 OPTIONS 请求在到达 Controller 之前就被拦截器、过滤器或者安全框架挡掉了返回的响应没有 CORS 头浏览器就直接认为预检失败真实请求根本不会发出。这也就是标题里描述的现象明明注解配置了前端还是跨域。1.3 从响应头角度理解配置生效链路不管是注解方式、配置类方式还是网关统一配置最终目的就是让响应头携带三样关键东西响应头字段作用Access-Control-Allow-Origin声明允许哪个来源读取响应*表示所有来源Access-Control-Allow-Methods声明允许哪些 HTTP 方法如 GET、POST、PUT、DELETEAccess-Control-Allow-Headers声明允许哪些自定义请求头如 Authorization、X-Token这三样字段缺少任何一个预检或真实请求都可能出问题。而且要注意响应头里的Access-Control-Allow-Origin如果设置为*浏览器还要求前端请求不要携带凭证信息比如 Cookie。因为带凭证的跨域请求不允许*必须精确指定域名。这个细节也是很多人的坑。理解了这一层才能明白排查重点在于找出哪个环节丢弃或覆盖了这些响应头。2. 为什么 CrossOrigin(origins *) 配置了却不生效的四大场景2.1 Spring Security 直接拦截了 OPTIONS 请求这是最高频的原因。项目里用了 Spring Security如果配置里没有对 OPTIONS 请求放行安全过滤器链会拦截所有请求包括预检请求。预检请求被 Security 拦截后返回的响应里通常不带 CORS 头浏览器就会判定预检失败。而且 Spring Security 还有一层自己的 CORS 配置机制。你可以启用http.cors()它会自动查找名为corsConfigurationSource的 Bean 或者 MVC 框架提供的 CORS 配置。如果你的项目里既在 Controller 上写了CrossOrigin又在 Security 配置里配了http.cors()两者之间可能发生配置覆盖或合并的混乱情况。我在实际项目里遇到过一种诡异的情况Security 配置里没有调用http.cors()但自定义了一个 CorsFilter 装载到过滤器链里结果该 Filter 的顺序不对被排在了 Spring Security 的过滤器之后导致预检请求先进了 Security被拦截后才轮到 CorsFilter响应头上被添加的 CORS 信息根本没有机会生效。2.2 自定义 Filter 或拦截器在作怪微服务项目里往往有自定义的登录校验过滤器、请求日志过滤器、统一响应包装过滤器等。这些过滤器如果在请求链路中提前返回了响应而没有继续执行后续的过滤器链就会跳过 Spring MVC 框架自动添加 CORS 头的逻辑。举个典型例子某个自定义 Filter 里对请求做了分发路由判断发现请求不符合条件就直接返回 401使用的是response.getWriter().write()没有做任何跨域头设置。这种情况下浏览器自然拿不到Access-Control-Allow-Origin。还有一种情况更隐蔽过滤器里使用了响应包装类比如ContentCachingResponseWrapper目的是记录响应体做日志。如果包装类没有正确透传响应头服务层设置的 CORS 头可能被丢掉。排查思路就在请求链路里把每个环节的关键响应头都打出来看看最后到底是谁动了头部。2.3 Controller 类与方法上的注解互相覆盖Spring 的CrossOrigin注解支持加在类上和方法上。很多人只把注解加在了方法上类上又没加。这本身没问题。但如果类里面有多个方法不同方法上的CrossOrigin配置不一致或者类上配置了某个域名方法上又配置了另一个域名Spring 内部会做合并处理合并逻辑有时不是你想的那样。比如类上有CrossOrigin(origins http://localhost:8080) public class UserController方法上有CrossOrigin(origins *) GetMapping(/list) public Result list()这种情况下Spring 会对两个注解的属性做合并origins被方法级别覆盖为*这没问题。但如果类上的注解没有把allowCredentials显式写为 false而方法上写的是 true合并出来就会带上Access-Control-Allow-Credentials: true。一旦响应头带上这个字段浏览器就会拒绝Access-Control-Allow-Origin: *的组合强制要求精确域名。这个坑很难一眼看出来因为注解本身写得看起来都合理。但浏览器报的错就那一句话The value of the Access-Control-Allow-Origin header in the response must not be the wildcard * when the requests credential mode is include。2.4 网关层 CORS 配置覆盖了服务层配置微服务架构里最让人头疼的就是这一层。请求链路通常是浏览器 - 网关 - 微服务。浏览器请求的目标地址是网关地址所以浏览器只关心网关返回的响应头。如果网关层没有配置 CORS或者配置方式和子服务的配置冲突子服务就算把 CORS 头加得再完整也没用。有一种情况是设置了全局 CORS 处理器比如基于 Spring Cloud Gateway 的GlobalCorsConfiguration同时在子服务里也加了CrossOrigin注解。网关处理完请求后附加了 CORS 头子服务处理完后也附加了 CORS 头两个头内容不一致时浏览器会以最终收到的头为准。但如果网关层对 OPTIONS 请求直接做了拦截不转发到下游只是返回空响应也不加任何 CORS 头那问题就变成 OPTIONS 完全没有人管。网关层还有一个典型问题当网关根据路径路由到不同服务时子服务返回的响应中原本的 CORS 头并不会被网关自动透传。有些网关过滤器会在转发过程中重写响应头导致 CORS 头丢失。3. 微服务场景下 CORS 配置的正确做法与实操方案3.1 网关层统一配置而不是分散到每个服务我的经验是在微服务架构里不要在子服务里依赖CrossOrigin注解处理跨域而是把 CORS 配置统一收敛到网关层。核心原因很简单浏览器最终面对的就是网关这个门面你在每个子服务里配置得再整齐经过网关一转发业务响应头上带的 CORS 信息可能就没了排查起来还要挨个服务看非常痛苦。如果用的是 Spring Cloud Gateway可以在配置文件里设置全局 CORS这是基于 WebFlux 的处理器配置方式如下spring: cloud: gateway: globalcors: cors-configurations: [/**]: allowed-origin-patterns: * allowed-methods: * allowed-headers: * allow-credentials: true max-age: 3600或者用 Java 配置类Configuration public class CorsGlobalConfiguration { Bean public CorsWebFilter corsWebFilter() { CorsConfiguration corsConfig new CorsConfiguration(); corsConfig.addAllowedOriginPattern(*); corsConfig.addAllowedMethod(*); corsConfig.addAllowedHeader(*); corsConfig.setAllowCredentials(true); corsConfig.setMaxAge(3600L); UrlBasedCorsConfigurationSource source new UrlBasedCorsConfigurationSource(); source.registerCorsConfiguration(/**, corsConfig); return new CorsWebFilter(source); } }用addAllowedOriginPattern而不是addAllowedOrigin在允许携带凭证时这一点很重要前者才能支持*与allowCredentials(true)的组合。这样网关层持有唯一的 CORS 策略子服务专注于业务逻辑跨域问题一举收敛。子服务已有的CrossOrigin注解建议移除避免双重配置出现奇怪的头部叠加。3.2 子服务不得不配时该选哪种方式子服务里有时出于接口复用或独立调试的考虑还是要在应用层配置 CORS。这种情况下我的建议是用 WebMvcConfigurer 的全局配置类而不是注解。原因是配置类可以集中管理权限范围更大还能避免每个 Controller 都要加注解的重复劳动。Configuration public class CorsConfig implements WebMvcConfigurer { Override public void addCorsMappings(CorsRegistry registry) { registry.addMapping(/**) .allowedOriginPatterns(*) .allowedMethods(GET, POST, PUT, DELETE, OPTIONS) .allowedHeaders(*) .allowCredentials(true) .maxAge(3600); } }这段代码全局生效。配合 Spring Security 时还需要在安全配置里调用http.cors()让 Spring Security 感知到 MVC 的 CORS 配置并纳入它的过滤器链。如果项目是 Spring WebFlux 风格比如基于 WebFlux 的响应式服务则要使用WebFluxConfigurer接口而不是WebMvcConfigurer。两个接口名字接近但所在的包完全不同用错了配置不会生效而且不会报任何错误。3.3 与 Spring Security 集成的注意事项Spring Security 6 的配置方式和旧版变化不小这里给出一段可以实际用的配置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(HttpMethod.OPTIONS, /**).permitAll() .anyRequest().authenticated() ) .formLogin(form - form.disable()) .httpBasic(basic - basic.disable()); return http.build(); } Bean public CorsConfigurationSource corsConfigurationSource() { CorsConfiguration configuration new CorsConfiguration(); configuration.addAllowedOriginPattern(*); configuration.addAllowedMethod(*); configuration.addAllowedHeader(*); configuration.setAllowCredentials(true); configuration.setMaxAge(3600L); UrlBasedCorsConfigurationSource source new UrlBasedCorsConfigurationSource(); source.registerCorsConfiguration(/**, configuration); return source; } }三个动作缺一不可显式声明corsConfigurationSource这个 Beanhttp.cors()开启并且对 OPTIONS 请求放行。很多同事只做了后两步没有显式声明 Bean这样 Spring Security 会去容器里自动查找CorsConfigurationSource如果找不到它就会内部建一个不完整的配置效果等于没有 CORS。3.4 前端调试时可以通过 fiddler 代理辅助定位后端 coder 排查跨域问题时如果办公室里有前端同事或者自己也装了抓包工具用 fiddler 这类代理工具辅助定位是非常快的。操作上很简单启动 fiddler 监听本机 8888 端口前端请求由它代理转发到你的后端接口请求路径改写后就能在 fiddler 的 Inspectors 面板里直接看到请求头、响应头的完整数据包括是否有Access-Control-Allow-Origin、Access-Control-Allow-Methods等字段。我一般建议的做法是先用 curl 直接调用后端接口或者用 fiddler 构造一个 Option 预检请求观察后端响应头的情况。如果 curl 都能拿到 CORS 头说明后端配置没问题问题很可能出在浏览器的判断逻辑或者前端请求方式上。如果 curl 拿不到那就是后端的锅顺着过滤器链往下查。curl 模拟预检请求的方法curl -i -X OPTIONS http://localhost:8080/api/user/list \ -H Origin: http://localhost:8081 \ -H Access-Control-Request-Method: GET \ -H Access-Control-Request-Headers: content-type,authorization看返回结果中是否有 CORS 相关字段一目了然。这也是我排跨域最快的办法比在浏览器里反复刷新看 Network 面板快得多。3.5 使用 PHP 或其他非 Java 后端时的参考思路虽然这个标题聚焦在微服务和CrossOrigin注解但群里也有写 PHP 后端的朋友来问跨域。基于最近搜到的“php 跨域 jsonp”这种热词说明还有不少老项目在用 jsonp 处理跨域。jsonp 的思路是利用script标签不受同源策略限制的特点让后端返回一段可执行脚本前端通过回调函数拿数据。但它存在明显局限只支持 GET 请求无法处理 POST、PUT 等复杂请求也已经不在新项目的主流方案里。如果是 PHP 后端做跨域配置主流做法是在中间件或入口文件中统一加响应头header(Access-Control-Allow-Origin: *); header(Access-Control-Allow-Methods: GET, POST, PUT, DELETE, OPTIONS); header(Access-Control-Allow-Headers: Content-Type, Authorization);需要注意的还是老问题如果请求需要携带 Cookie 或认证信息就不能用*得精确指定来源域名并加上Access-Control-Allow-Credentials: true。这一点规则在所有语言的后端里是通用的。Java 微服务排障时也可以参考同样的头部检查原则先确认服务返回的 CORS 头是否符合前端的请求模式。4. 现场排障实录与问题速查表4.1 一个完整的排查过程记录说一个我印象比较深的真实排障过程。某个项目前端是 Vue 3 部署在 8080 端口后端网关是 Spring Cloud Gateway 部署在 9000 端口子服务是用户服务跑在 8082 端口。前端请求http://localhost:9000/api/user/info带上Authorization头。浏览器报错Access to XMLHttpRequest has been blocked by CORS policy。后端同事说网关里已经配了全局 CORS子服务控制器上也加了CrossOrigin(origins *)。听起来配置很全但就是不行。我当时做的第一步是用 fiddler 抓包先看 OPTIONS 请求到底打到哪、返回什么。结果发现 OPTIONS 请求根本没打到网关还在浏览器层面就被拦下了。仔细一看前端代码里 axios 请求的withCredentials被设置成了true并且网关的 CORS 配置又设置了allowCredentials: true同时 allowed-origin-patterns 配的是*。按照 CORS 规范带凭证信息时不允许通配符必须指定具体来源。这个问题的修复方式不是把*改掉而是在前端明确使用具体的请求来源地址网关配置allowedOriginPatterns精确到前端域名。如果前端地址经常变比如本地开发和测试环境不同也可以在网关层维护一个域名白名单列表。替换配置后重启网关浏览器请求恢复正常。这种问题最磨人的地方在于你看了后端配置会觉得明明都对但浏览器就是跟你对着干。所以建议前端代码里尽量少用withCredentials: true如果业务上确实要带 Cookie 做单点登录那么后端每个 CORS 配置都要注意来源精确匹配并且不能跨服务共享一个全局*。4.2 高频症状对应根因速查表症状可能根因快速验证方式OPTIONS 请求返回 401 / 403Spring Security 未放行预检检查安全配置中是否对 OPTIONS 做了 permitAllOPTIONS 请求返回 200 但无 CORS 头拦截器或过滤器提前返回响应抓包看响应头确认过滤器链路中是否跳过滤链响应头有Allow-Origin: *同时有Allow-Credentials: true带凭证跨域不允许通配符精确指定来源域名不要用*GET 请求正常POSTapplication/json跨域预检未通过确认 OPTIONS 请求能否正常返回 CORS 头子服务接口用 curl 调用有 CORS 头前端访问网关没有网关层未透传或覆盖在网关层统一配置 CORS本地改了配置对方说还是不行浏览器缓存了 OPTIONS 结果启动时配置maxAge调小或清缓存验证4.3 两条压箱底的排查经验第一跨域问题永远从响应头入手而不是从代码注释里抠。用 curl 或者抓包工具先拿到原始响应看看有没有 CORS 头没有就顺着链路找哪个环节丢弃了有就看头里的配置值是否和前端期望匹配。这个思路能帮你绕开 80% 的弯路。第二微服务架构中网关层的 CORS 配置优先级最高。如果网关已经配好了子服务尽量不要重复配置。有时候网络上下文里的配置你自己看觉得没问题但一旦多个配置叠加某个中间环节意外合并了两个头就会出现“单看都对拼起来就坏”的情况。4.4 本地开发连不上网关时的小技巧本地开发时很多前端同事不愿意把所有请求都走网关图省事直接请求子服务端口。但子服务的 CORS 配置和网关注定是两套逻辑于是跨域问题就一个个冒出来。我的建议是尽量让前端开发环境也走网关保证和生产环境链路一致。实在不行就自己加一层代理Vite 或 webpack 的 devServer 配置代理是最轻量的方式// vite.config.js server: { proxy: { /api: { target: http://localhost:9000, changeOrigin: true } } }这样前端请求看起来就是同源请求浏览器层面根本不会触发跨域开发体验能提升一大截。不过这只是开发期的绕行方案生产环境的跨域策略一定还是要靠后端正确配置去解决。5. 针对网关与子服务组合场景的方案建议5.1 子服务保留注解但网关不做检查时的配置参考有的团队代码版权或者历史包袱太重子服务里面的CrossOrigin注解暂时没法删这时最重要的就是要保持配置结果的一致性。网关层尽量不要再设置 allowCredentials避免和子服务注解产生叠加冲突。如果子服务里很多 Controller 都写了自己的CrossOrigin注解而且内容还不统一有时间的话还是建议周末统一改成配置类方式既能一致管理也方便后续调整。用注解临时顶着的项目常常过了几个月就没人敢动那一层跨域问题变成历史遗留问题。5.2 网关统一 CORS 后子服务怎么移除注解移除CrossOrigin其实没有技术难点工作量大头在梳理接口。可以先通过 IDE 全局搜索找出所有使用CrossOrigin的地方评估每个 Controller 对外的访问来源然后把配置整合到网关注入配置文件里。这里有一个要注意的小细节如果接口里用到了HttpServletResponse手动设置响应头比如自己写了response.setHeader(Access-Control-Allow-Origin, *);那这种代码和网关配置也会出现冲突。建议全局搜索setHeader和Access-Control关键字把所有手动设置统一删掉。5.3 如果前端强制要求 JSONP 的兼容处理现实中偶尔会遇到历史遗留的前端页面用了 JSONP 方式已经没有改动计划。这时后端可以在特定接口上支持 JSONP 响应格式。思路是拦截请求的callback参数如果存在则返回一段 JavaScript 包装的数据GetMapping(/jsonp) public String jsonp(RequestParam(callback) String callback, String data) { return callback ( data ); }这种方式只能说应急不要用来设计新接口。JSONP 的安全风险明显无法支持 POST、无法感知请求完成状态而且与微服务网关配置 CORS 的设计方向相违背。我个人的观点是能统一 CORS 就统一JSONP 只作为兼容历史接口的过渡手段。6. 最后再分享一个我自己总结的排障小工具排障的时候不建议每次到浏览器控制台里翻红字半天看不出原因。我习惯先在浏览器控制台手动发一段模拟代码直接把结果打印出来看fetch(http://localhost:9000/api/user/info, { method: GET, headers: { Content-Type: application/json, Authorization: Bearer xxx } }) .then(res res.text()) .then(text console.log(text)) .catch(err console.warn(err));如果这个代码在跨域时报错重点看err对象内部的 message浏览器给出的错误信息往往直接把问题指向了某类配置。比如 Response to preflight request doesnt pass access control check: No Access-Control-Allow-Origin header is present on the requested resource那基本就能确定响应里没有 CORS 头接下来就去查过滤器链和网关配置。再分享一个经验处理这种问题不要一个一个接口去试先把所有接口路径的配置文件或者注解写法和请求模式梳理出来统一检查一遍比啥日志都来得快。自测阶段每条核心链路至少过一遍简单 GET 请求、POST JSON 请求、带自定义头的请求基本就能把跨域配置的坑提前排掉。这些经验碰到过实际问题才会知道有多值钱。配置看似几行代码但微服务链路上的每个节点都在等你出错。希望这篇内容能让你少走一次弯路。
返回列表