广告位 · 页头(后台-模块管理-新建模块后替换此区域)

Spring Boot 接口跨域预检请求(OPTIONS)失败如何解决?

跨域问题在前后端分离项目里很常见,其中预检请求失败又比普通 CORS 报错更让人困惑:浏览器控制台可能会显示 Response to preflight request doesn‘t pass access control check,实际请求却根本没有发出。这个报错的核心在于浏览器先发送了一个 OPTIONS 预检请求,服务端没有正确响应。下面把 Spring Boot 项目中 OPTIONS 失败的原因、配置方法和排查思路完整梳理一遍。

Spring Boot 接口跨域预检请求(OPTIONS)失败如何解决?

一、为什么要先发 OPTIONS 预检请求?

浏览器出于同源策略限制,不会直接允许跨域请求访问另一个域下的接口。对于简单请求,比如使用 GET、POST 且 Content-Type 为 text/plain 或表单编码的请求,浏览器会直接发送,并通过响应头判断是否允许读取。但对于非简单请求,情况就更复杂,比如前端使用 application/json 作为 Content-Type,或者自定义了 Authorization、X-Requested-With 等请求头,又或者请求方法为 PUT、DELETE,浏览器会先发送一个 OPTIONS 预检请求,询问服务器是否允许即将发生的跨域操作。

这个 OPTIONS 请求会携带两个关键请求头:Access-Control-Request-Method 表示真实请求要使用的方法,Access-Control-Request-Headers 表示真实请求要携带的自定义头。服务器必须明确返回 Access-Control-Allow-Origin、Access-Control-Allow-Methods、Access-Control-Allow-Headers 等响应头,浏览器才认为预检通过,随后才会发出真正的业务请求。如果服务端没有配置 CORS,或者配置不完整,又或者 OPTIONS 请求被 Spring Security 拦截,预检就会失败,业务请求根本不会执行。

在 Spring Boot 项目里,预检失败的原因通常可以归纳为四类:没有引入任何 CORS 配置;虽然配置了 CORS 但 allowedMethods 没有包含 OPTIONS;allowedOrigins 设置为星号同时 allowCredentials 又设置为 true,导致配置直接报错或无法生效;引入 Spring Security 后,过滤链把未认证的 OPTIONS 请求当作普通请求拦截下来。理解了这些原因,接下来的处理就会清晰很多。

二、先配置好 CorsConfigurationSource

Spring Boot 中解决跨域最常用的方式是定义 CorsConfigurationSource Bean,它同时可以被 Spring MVC 和 Spring Security 使用,适合需要全局统一配置的项目。相比在每个控制器上使用 @CrossOrigin 注解,集中配置更容易维护,也避免了遗漏某个接口导致部分跨域依然失败的问题。

下面是一个比较完整的 CorsConfigurationSource 配置示例:

@Configuration
public class CorsConfig {
    @Bean
    public CorsConfigurationSource corsConfigurationSource() {
        CorsConfiguration config = new CorsConfiguration();
        config.setAllowedOriginPatterns(List.of("*"));
        config.setAllowedMethods(List.of("GET", "POST", "PUT", "DELETE", "OPTIONS"));
        config.setAllowedHeaders(List.of("*"));
        config.setAllowCredentials(true);
        config.setMaxAge(3600L);
        UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
        source.registerCorsConfiguration("/**", config);
        return source;
    }
}

这里有几个关键点需要说明。第一,使用 setAllowedOriginPatterns 而不是 setAllowedOrigins,因为当 allowCredentials 为 true 时,allowedOrigins 不能使用星号,否则 Spring 会抛出 IllegalArgumentException,而 allowedOriginPatterns 允许使用星号通配符。第二,setAllowedMethods 中必须显式包含 OPTIONS,很多人配置时只写了 GET、POST、PUT、DELETE,结果预检请求被 CORS 配置自己拒绝。第三,allowedHeaders 设置得越宽松,前端能携带的自定义请求头就越多,如果需要携带 Authorization,这里要确保允许。

有时开发者也会选择 WebMvcConfigurer 的 addCorsMappings 方式,比如直接重写该方法来添加映射规则。这种写法在纯 Spring MVC 场景下有效,但如果项目里同时使用了 Spring Security,安全过滤链通常会在 MVC 之前处理请求,addCorsMappings 配置不一定能覆盖预检请求。因此一旦引入 Security,更推荐使用 CorsConfigurationSource,并在 Security 配置中显式启用它。

三、在 Spring Security 中放行 OPTIONS 请求

Spring Security 的过滤器链位于 Spring MVC 之前,任何进入应用的请求都会先经过安全过滤链。默认情况下,未认证的请求会被拦截并返回 401,OPTIONS 预检请求也不例外。即使已经定义了 CorsConfigurationSource,如果 Security 过滤链没有启用 CORS 处理,预检请求也会因为缺少认证信息而被拒绝。所以处理 Spring Boot 跨域预检失败,往往需要同时修改 CORS 配置和安全配置。

推荐的安全配置写法如下:

@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
    http.cors().and()
        .csrf().disable()
        .authorizeHttpRequests(auth -> auth
            .requestMatchers(HttpMethod.OPTIONS, "/**").permitAll()
            .anyRequest().authenticated()
        );
    return http.build();
}

上面的代码先通过 http.cors() 启用 CORS 支持,Security 会自动寻找容器中的 CorsConfigurationSource 并应用。随后用 requestMatchers(HttpMethod.OPTIONS, "/**").permitAll() 把所有 OPTIONS 请求直接放行,不进入认证逻辑。这样即使前端携带了自定义头,预检请求也能顺利通过,真实请求依然会按 anyRequest().authenticated() 进行认证。

如果项目中使用了自定义 JWT 过滤器,还需要额外注意:不要在过滤器中对 OPTIONS 请求解析 Token 或校验权限。很多 JWT 过滤器会从请求头里获取 Token,而预检请求通常不会携带 Token,解析失败就会返回 401,导致预检失败。正确的做法是在过滤器开头判断请求方法,如果是 OPTIONS,直接放行给下一个过滤器处理。增加几行短路逻辑后,可以避免非常多的隐藏问题。

四、排查预检失败的方法和常见问题

遇到 OPTIONS 预检失败,首先应该打开浏览器的 Network 面板,找到对应的 OPTIONS 请求,查看它的状态码和响应头。正常预检应当返回 200,并且响应头中包含 Access-Control-Allow-Origin、Access-Control-Allow-Methods、Access-Control-Allow-Headers。如果状态码是 401 或 403,大概率是 Security 或权限过滤器拦截了预检;如果状态码是 200 但缺少某个 Access-Control-Allow-* 头,则是 CORS 配置不完整。

除了浏览器面板,也可以用 curl 模拟预检请求,快速确认服务端返回:

curl -X OPTIONS -i -H 'Origin: http://localhost:3000' -H 'Access-Control-Request-Method: POST' -H 'Access-Control-Request-Headers: content-type' http://localhost:8080/api/test

实际排查中还要注意几个高频错误。一个是 allowedOrigins 和 allowCredentials 同时使用导致配置异常,解决方法是改用 allowedOriginPatterns。另一个是 allowedMethods 漏掉 OPTIONS,Spring 会在响应中告诉浏览器不允许该方法,预检自然失败。还有 allowedHeaders 配置过窄,比如前端带了 Content-Type 之外的自定义头,但服务端只允许 Content-Type,也会导致预检失败。此外,maxAge 设置过短只是性能问题,但如果设置过长,客户端可能会缓存过期的 CORS 结果,排障时可以留意一下这个值。

如果配置看起来都正确,但仍然失败,可以检查是否有多套 CORS 配置互相覆盖。比如同时定义了 CorsConfigurationSource 和 WebMvcConfigurer 的 addCorsMappings,或者同时使用了网关层的 CORS 配置,可能会造成两个配置都不完整地处理 OPTIONS 请求。此时建议只保留一套统一的配置源,避免规则冲突。

五、总结

Spring Boot 接口的 OPTIONS 预检失败,本质上不是单一配置问题,而是 CORS 机制和 Spring Security 过滤链共同作用的结果。解决时至少要完成三件事:定义 CorsConfigurationSource,正确设置 allowedOriginPatterns、allowedMethods 和 allowedHeaders;在 Security 配置中启用 cors() 并放行所有 OPTIONS 请求;如果存在自定义过滤器,需要避免过滤器对预检请求做认证或业务处理。

在修复之后,建议用浏览器 Network 面板和 curl 分别验证一次,确认 OPTIONS 请求返回 200,并且响应头完整。一旦预检成功,真实的 POST、PUT、DELETE 请求就会正常发出,前端跨域问题也就自然解决。

广告位 · 文章底部(后台-模块管理替换此区域)