首页 / 资讯中心 / 文章详情

SpringBoot跨域CORS配置全解析:原理、方案与生产环境避坑指南

SpringBoot跨域CORS配置全解析:原理、方案与生产环境避坑指南 ★ FEATURED ARTICLE
1. 跨域到底卡在哪儿先搞懂浏览器那点心思前后端分离开发早就成了默认姿势前端跑在localhost:5173后端 SpringBoot 跑在localhost:8080接口一调控制台红字一片Access to XMLHttpRequest at http://localhost:8080/api/xxx from origin http://localhost:5173 has been blocked by CORS policy。这个场景我见过太多次了新手第一反应往往是“我接口明明能通Postman 里好好的怎么浏览器就不行”然后开始怀疑人生。先把结论摆在这儿跨域不是后端接口坏了是浏览器在替你做安全拦截。浏览器有个同源策略协议、域名、端口三者必须完全一致才算同源。5173和8080端口不同直接判定跨域。Postman、curl 这类工具不走浏览器那套策略所以它们永远测不出跨域问题——这也是为什么很多人本地用 Postman 调得好好的一上浏览器就翻车。那浏览器为什么要拦核心是防止恶意站点拿着用户在 A 站的登录态去偷偷请求 B 站的接口。比如你登录了某银行网站cookie 还在这时候打开一个恶意页面它偷偷发请求到银行接口转账——同源策略就是拦这个的。所以跨域限制本质是保护用户不是为难开发者。CORS跨域资源共享就是官方给出的“合法放行”机制。服务端通过响应头告诉浏览器“这个来源我允许这些方法我允许这些头我允许”。浏览器看到响应头匹配才把真正的响应交给前端 JS。理解这一点非常关键因为后面所有的配置本质上都是在往响应头里塞正确的字段。SpringBoot 处理跨域说穿了就三条路全局配置、注解配置、过滤器/拦截器手动写头。三条路各有适用场景选错了要么不生效要么埋坑。下面我按“为什么这么选—怎么配—踩过什么坑”的顺序把这块彻底讲透。适合正在做前后端分离、被跨域折磨过的同学也适合想搞清楚 CORS 底层原理的进阶读者。2. 三种主流方案怎么选别一上来就抄配置2.1 全局配置项目里最省心的默认选择绝大多数项目我推荐直接用全局配置。原因很简单一个项目里跨域规则通常是统一的全局配一次所有接口自动生效不用每个 Controller 去贴注解。SpringBoot 提供了WebMvcConfigurer接口重写addCorsMappings方法即可。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); } }这段代码看着简单但每一行都有讲究我逐个拆。addMapping(/**)表示拦截所有路径。注意这里是 Spring 的路径匹配/**能覆盖多级路径别写成/*那样只能匹配一级。allowedOriginPatterns(*)和allowedOrigins(*)的区别是新手最容易踩的坑。当allowCredentials(true)时allowedOrigins(*)会直接抛异常因为规范不允许“允许所有来源”和“携带凭证”同时成立。allowedOriginPatterns是 Spring 后来提供的替代方案它支持通配符且能和凭证共存。所以只要你的项目涉及 cookie、session、Authorization 头就必须用allowedOriginPatterns。allowCredentials(true)表示允许前端携带凭证cookie、认证头。如果你的登录态是基于 cookie 的 session这个必须开如果用的是 JWT 放在 header 里其实可以不开但开了也无妨。maxAge(3600)是预检请求的缓存时间单位秒。这个参数后面会重点讲它直接关系到性能。2.2 注解配置适合接口级别精细控制有时候你不想全局放开只想给某几个接口开跨域那就用CrossOrigin注解。它可以打在类上也可以打在方法上。RestController RequestMapping(/api/user) CrossOrigin(origins http://localhost:5173, maxAge 3600) public class UserController { GetMapping(/info) public User info() { return userService.getCurrent(); } }注解的优先级高于全局配置也就是说如果全局配了*某个接口用注解限定了具体来源那这个接口就按注解走。这个特性在“大部分接口放开、个别敏感接口收紧”的场景下很有用。但注解有个明显的短板分散。一个项目几十个 Controller每个都贴注解改规则的时候要满项目找维护成本高。所以我一般只在临时调试、或者确实需要差异化规则的少数接口上用注解主力还是全局配置。2.3 过滤器手动写头理解原理的最佳途径第三种是自己写Filter手动往响应里塞 CORS 头。这种方式现在用得少但强烈建议每个后端都手写一遍因为写完你就彻底明白 CORS 是怎么回事了。Component public class CorsFilter implements Filter { Override public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) throws IOException, ServletException { HttpServletResponse response (HttpServletResponse) res; HttpServletRequest request (HttpServletRequest) req; response.setHeader(Access-Control-Allow-Origin, request.getHeader(Origin)); response.setHeader(Access-Control-Allow-Methods, GET,POST,PUT,DELETE,OPTIONS); response.setHeader(Access-Control-Allow-Headers, *); response.setHeader(Access-Control-Allow-Credentials, true); response.setHeader(Access-Control-Max-Age, 3600); if (OPTIONS.equalsIgnoreCase(request.getMethod())) { response.setStatus(HttpServletResponse.SC_OK); return; } chain.doFilter(req, res); } }注意这里Access-Control-Allow-Origin我写的是request.getHeader(Origin)而不是*。原因还是那个带凭证时不能用*必须回显具体来源。这也是手写过滤器最容易出错的地方。三种方案对比一下方案适用场景优点缺点全局配置项目统一规则一处配置全局生效粒度粗注解配置个别接口差异化粒度细、直观分散难维护过滤器需要底层控制灵活、能理解原理手写易错我的建议是新项目直接上全局配置遇到特殊接口再叠加注解。过滤器作为理解工具写一遍就够了。3. 预检请求那个让你接口“调两次”的元凶3.1 简单请求与复杂请求的分界线很多人发现一个诡异现象前端发一个 POST后端日志里却出现了两次请求一次是 OPTIONS一次才是真正的 POST。这就是预检请求Preflight。浏览器把跨域请求分成两类。简单请求直接发不预检。满足以下全部条件才算简单请求方法是 GET、HEAD、POST 之一请求头只包含 Accept、Accept-Language、Content-Language、Content-Type 等少数几个且 Content-Type 只能是application/x-www-form-urlencoded、multipart/form-data、text/plain三种之一。只要有一条不满足就是复杂请求浏览器会先发一个 OPTIONS 请求去“问路”我能用这个来源、这个方法、这些头访问你吗服务端回一堆Access-Control-Allow-*头说“可以”浏览器才发真正的请求。这里有个高频坑前端用application/json发 POSTContent-Type 是application/json不在简单请求的白名单里所以必然触发预检。这就是为什么现在前后端分离项目里 OPTIONS 请求满天飞。3.2 预检请求为什么会被 401/403 拦掉预检请求有个特点它不携带业务凭证。浏览器发 OPTIONS 时不会带上你的 JWT 或 cookie。如果你的项目里配了全局的登录拦截器或 Spring SecurityOPTIONS 请求因为没带 token直接被拦下来返回 401预检失败真正的请求根本发不出去。这个坑我踩过不止一次现象是前端报 CORS 错误但后端日志里 OPTIONS 返回 401。排查思路很明确让 OPTIONS 请求绕过认证拦截。如果用的是 Spring Security需要放行 OPTIONShttp.authorizeHttpRequests(auth - auth .requestMatchers(HttpMethod.OPTIONS, /**).permitAll() .anyRequest().authenticated());如果用的是自己写的拦截器在preHandle里判断Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) { if (HttpMethod.OPTIONS.matches(request.getMethod())) { return true; } // 后续认证逻辑 }提示OPTIONS 请求放行是跨域配置里最容易被忽略的一环。只要你的项目有登录拦截就必须显式放行 OPTIONS否则跨域永远调不通。3.3 maxAge 到底省了多少次请求预检请求是有成本的每次复杂请求都要多一次往返。Access-Control-Max-Age就是告诉浏览器“这个预检结果你缓存起来多少秒内别再问了”。设成 3600意味着一个小时内同一个接口的复杂请求不再发预检直接发真实请求。对于接口调用频繁的页面这个优化效果非常明显。我实测过一个列表页一屏加载触发十几个复杂请求不设 maxAge 就是二十多次请求设了之后第一次预检完后面全是真实请求请求数直接砍半。但 maxAge 也不是越大越好。如果你在调试阶段频繁改跨域规则浏览器缓存着旧的预检结果你会看到“配置改了却不生效”的假象。调试期建议设小一点甚至设 0上线前再调大。另外不同浏览器对 maxAge 有上限Chrome 大概限制在 2 小时左右设太大也会被截断。4. 生产环境的坑从本地能跑到线上能用4.1 本地*一时爽上线凭证全泡汤开发阶段图省事很多人直接allowedOrigins(*)本地确实能跑。但一旦项目涉及登录前端请求带了 cookie 或 Authorization 头浏览器就会拒绝——因为带凭证时*不合法。这时候你会看到一种很迷惑的现象不带登录态的接口正常一带登录态就跨域失败。正确做法是明确列出允许的来源或者用allowedOriginPatterns。生产环境更推荐白名单把前端实际部署的域名写进去registry.addMapping(/**) .allowedOrigins(https://your-frontend-domain.com) .allowedMethods(GET, POST, PUT, DELETE, OPTIONS) .allowedHeaders(*) .allowCredentials(true) .maxAge(3600);白名单的好处是安全坏处是前端域名一变就要改配置重启。如果前端有多个环境测试、预发、生产可以把来源做成配置项从配置文件读避免硬编码。4.2 网关、Nginx 与 SpringBoot 的“三重跨域”微服务架构下请求链路往往是浏览器 → Nginx → 网关 → SpringBoot 服务。这时候跨域头可能在多个环节被处理出现重复的Access-Control-Allow-Origin头浏览器直接报错“The Access-Control-Allow-Origin header contains multiple values”。原因通常是 Nginx 配了一份跨域SpringBoot 又配了一份两个头叠加。解决办法是只在一层处理跨域。我的习惯是如果用了 Nginx 或网关跨域统一在那一层做SpringBoot 里就不配了如果没网关就在 SpringBoot 里配。千万别两头都配。Nginx 层配置大概长这样location /api/ { add_header Access-Control-Allow-Origin $http_origin always; add_header Access-Control-Allow-Credentials true always; add_header Access-Control-Allow-Methods GET,POST,PUT,DELETE,OPTIONS always; add_header Access-Control-Allow-Headers * always; if ($request_method OPTIONS) { return 204; } proxy_pass http://backend; }注意always参数它保证即使返回错误码比如 4xx、5xx也带上跨域头。不加的话接口报错时跨域头丢失前端看到的是 CORS 错误而不是真正的业务错误排查起来非常痛苦。4.3 前端代理开发期最优雅的绕行方案严格来说前端开发服务器代理不算“解决跨域”而是绕过跨域。原理是让前端请求发给同源的开发服务器由开发服务器转发到后端浏览器看来是同源请求自然没有跨域问题。Vite 的配置// vite.config.js export default { server: { proxy: { /api: { target: http://localhost:8080, changeOrigin: true, rewrite: (path) path.replace(/^\/api/, ) } } } }changeOrigin: true会把请求头里的 Host 改成目标地址很多后端会校验 Host不开这个可能被拒。这个方案在开发期非常香前端不用管后端跨域配置各写各的。但要注意它只在开发期有效打包上线后代理就没了。上线后要么前后端同域部署要么后端老老实实配 CORS。所以代理是开发利器不是生产方案别搞混。5. 常见问题速查与排查心法5.1 一张表搞定高频报错跨域报错信息五花八门但翻来覆去就那几类。我整理了一张速查表遇到问题先对号入座报错信息关键词根本原因解决方向No Access-Control-Allow-Origin header服务端没返回跨域头检查配置是否生效、路径是否匹配header contains multiple values多层重复配置只保留一层跨域配置credentials mode is include 但 origin 为 *带凭证却用了通配符改用 allowedOriginPatterns 或白名单OPTIONS 请求返回 401/403预检被认证拦截放行 OPTIONS 请求配置改了不生效预检结果被缓存清缓存或调小 maxAge接口报错时跨域头消失错误响应没带跨域头Nginx 加 always或全局异常处理补头5.2 排查跨域的三步定位法遇到跨域问题别急着改代码按这个顺序走能省大量时间。第一步看浏览器 Network 面板。找到失败的那个请求看它是 OPTIONS 还是真实请求。如果是 OPTIONS 失败问题在预检如果 OPTIONS 成功但真实请求失败问题在真实请求的响应头。第二步看 Response Headers。重点看有没有Access-Control-Allow-Origin值是不是当前来源。如果压根没有这个头说明服务端配置没生效去查配置类有没有被扫描到、路径匹配对不对。第三步看后端日志。如果 Network 里 OPTIONS 返回 401/403但后端业务日志没打印说明请求被拦截器挡在业务逻辑之前了去查认证配置。这三步走下来九成跨域问题都能定位。我见过太多人一上来就疯狂改配置结果方向都错了。5.3 几个反直觉的细节跨域失败时后端可能根本没收到请求。如果是预检失败真实请求压根不会发出后端自然没日志。所以“后端没日志”不代表“请求没到后端”要看是哪个阶段失败的。CORS 错误可能掩盖真实错误。接口返回 500但响应没带跨域头浏览器只报 CORS 错误你看不到 500。这时候要么用 Postman 直接调接口看真实状态要么确保错误响应也带跨域头。CrossOrigin加在 Controller 上但请求被 Filter 拦了。注解是在 Spring MVC 层处理的如果请求在 Filter 阶段就被拦截返回注解根本没机会生效。这是注解方案的一个隐藏限制。提示调试跨域时先把浏览器缓存禁用DevTools 的 Network 面板勾选 Disable cache避免预检缓存干扰判断。6. 我踩过的坑与几条实在建议说几个我实际项目里踩过的坑都是文档里不会写的。坑一allowedHeaders(*)在某些场景不够用。虽然通配符理论上允许所有头但个别浏览器对自定义头比如X-Token、X-Request-Id的处理有差异预检时可能被拒。稳妥做法是把前端实际用到的自定义头显式列出来比如allowedHeaders(Content-Type, Authorization, X-Token)。坑二Cookie 的 SameSite 属性。跨域带 cookie 时如果 cookie 的SameSite是Strict或Lax浏览器可能不发。跨站场景需要设成SameSiteNone; Secure且必须走 HTTPS。这个坑和 CORS 是两回事但现象很像容易混淆。坑三本地用 IP 访问配置里写的是 localhost。127.0.0.1和localhost在浏览器看来是不同来源白名单里只写了localhost用 IP 访问就跨域失败。调试时统一用一个别混着来。坑四SpringBoot 版本差异。SpringBoot 2.4 之前和之后allowedOrigins和allowedOriginPatterns的行为有变化老项目升级时跨域配置可能要跟着改。升级前先确认版本对应的 API。最后给几条实在建议。跨域配置优先全局、白名单、明确方法别图省事用通配符尤其是生产环境。OPTIONS 放行是必做项有认证就有预检。只在一层处理跨域Nginx 和 SpringBoot 二选一。调试期关缓存、调小 maxAge上线前再优化。还有最重要的一条跨域报错先看 Network别瞎改代码方向对了问题就解决了一半。这套东西我前后在好几个项目里反复验证过从单体到微服务从本地到线上核心逻辑没变过。把原理吃透配置就是顺手的事原理不清抄再多配置也是碰运气。
阅读完成 · 觉得有帮助?
咨询建站