跨域问题几乎都出现在前后端分离架构里。浏览器出于同源策略限制,当一个请求从 http://localhost:5173 发到 http://localhost:8080 时,即使后端接口能正常处理并返回数据,浏览器也会把响应拦截下来,前端只能看到 CORS policy 报错。Java 后端要做的不是放开所有限制,而是按接口范围返回一组标准的跨域响应头,让浏览器完成校验。

接下来从请求类型、Spring Boot 配置、原生 Servlet、Nginx 以及排查方法几个角度展开。
一、先判断请求类型,才能知道要返回什么响应头
CORS 的表象是浏览器拦截响应,但服务端要做的事情并不是复杂地放行域名,而是根据请求类型补齐响应头。浏览器把跨域请求分成简单请求和需要预检的请求。
简单请求通常满足三个条件:请求方法为 GET、HEAD、POST;Content-Type 限于 application/x-www-form-urlencoded、multipart/form-data、text/plain;没有自定义请求头。这类请求不会先发 OPTIONS,浏览器直接携带 Origin 字段发起请求,服务器只要返回 Access-Control-Allow-Origin 即可。反之,一旦使用了 application/json、PUT、DELETE 或自定义头 Authorization,浏览器就会先发一个 OPTIONS 预检请求,询问服务器是否允许后续真实请求。
如果 Java 服务端只处理 GET/POST 业务路径,但没处理 OPTIONS,预检会直接失败,前端会看到 CORS policy: Response to preflight request doesn't pass access control check。这是 Java 后端最常见的跨域故障之一。
| 请求类型 | 触发条件 | 服务端处理 |
|---|---|---|
| 简单请求 | GET/HEAD/POST,Content-Type 受限,无自定义头 | 返回 Access-Control-Allow-Origin |
| 预检请求 | PUT、DELETE、application/json、自定义头 | 先响应 OPTIONS,再放行真实请求 |
二、Spring Boot 的标准配置方式
Spring 框架提供了从注解到全局过滤器的多级支持,适合不同粒度。
2.1 使用 @CrossOrigin 注解做局部放行
如果只有少数接口需要跨域,可以直接在 Controller 或方法上加 @CrossOrigin。它的优点是粒度细,缺点是如果项目接口很多,到处加注解反而难维护。
@RestController
@RequestMapping("/api")
@CrossOrigin(origins = "http://localhost:5173", maxAge = 3600)
public class UserController {
@GetMapping("/info")
public Map<String, Object> getInfo() {
return Map.of("name", "测试用户");
}
}
注解的 origins 参数可以传入多个来源,maxAge 用于设置预检结果缓存时间,单位是秒。allowCredentials 默认不开启,如果需要携带 Cookie,需要显式设置为 true,同时 origins 不能写成星号。
2.2 通过 WebMvcConfigurer 做全局配置
更常见的是把所有 /api/** 路径统一处理,集中配置 allowedOrigins、allowedMethods、allowedHeaders。可以考虑继承 WebMvcConfigurer 实现 addCorsMappings 方法。
@Configuration
public class CorsConfig implements WebMvcConfigurer {
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/api/**")
.allowedOrigins("http://localhost:5173", "https://app.ipipp.com")
.allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS")
.allowedHeaders("*")
.allowCredentials(true)
.maxAge(3600);
}
}
上面这个配置表示只有 /api 开头的接口会返回 CORS 头,来源列表明确写死,不向任意站点开放。allowedHeaders 使用星号表示接收所有请求头,生产环境如果对安全要求高,建议改成 Content-Type、Authorization 等实际使用字段。
2.3 使用 CorsFilter Bean 进行更早拦截
WebMvcConfigurer 是 MVC 层面的处理,如果项目里有非 MVC 的 Servlet 或自定义 Filter,更推荐用 CorsFilter Bean,它会在过滤链较早位置生效。
@Bean
public CorsFilter corsFilter() {
CorsConfiguration config = new CorsConfiguration();
config.addAllowedOrigin("http://localhost:5173");
config.addAllowedMethod("*");
config.addAllowedHeader("*");
config.setAllowCredentials(true);
config.setMaxAge(3600L);
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/api/**", config);
return new CorsFilter(source);
}
CorsFilter 与 WebMvcConfigurer 二选一即可,同时配置可能造成响应头重复。如果项目使用 Spring Security,单独配置 WebMvcConfigurer 通常不够,因为 OPTIONS 请求可能在到达 MVC 之前就被安全过滤链拦截。
2.4 结合 Spring Security 放行预检请求
Spring Security 会拦截所有请求,包括预检 OPTIONS。需要在 SecurityFilterChain 里启用 CORS,并显式放行 OPTIONS。可以这样配置:
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http.cors(Customizer.withDefaults())
.authorizeHttpRequests(auth -> auth
.requestMatchers(HttpMethod.OPTIONS, "/**").permitAll()
.anyRequest().authenticated()
)
.httpBasic(Customizer.withDefaults());
return http.build();
}
这里 http.cors() 会尝试从 Spring 容器中读取 CorsConfigurationSource。如果已经定义了 CorsFilter 或 WebMvcConfigurer,Spring Security 会自动复用相应策略。如果不希望为预检请求单独写放行规则,也可以把 CORS 配置提升到安全链之前,但显式 permitAll 会更清晰直观。
三、原生 Servlet 与 Nginx 反向代理方案
不是所有 Java 项目都用 Spring Boot,原生 Servlet、老框架或前后端中间有 Nginx 的项目同样需要 CORS 支持。手写过滤器比较直接,适合不想引入额外依赖的场景。
@WebFilter("/*")
public class SimpleCorsFilter implements Filter {
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
throws IOException, ServletException {
HttpServletResponse resp = (HttpServletResponse) response;
HttpServletRequest req = (HttpServletRequest) request;
resp.setHeader("Access-Control-Allow-Origin", "http://localhost:5173");
resp.setHeader("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS");
resp.setHeader("Access-Control-Allow-Headers", "Content-Type, Authorization");
resp.setHeader("Access-Control-Max-Age", "3600");
if ("OPTIONS".equalsIgnoreCase(req.getMethod())) {
resp.setStatus(HttpServletResponse.SC_OK);
return;
}
chain.doFilter(request, response);
}
}
如果应用前面挂了 Nginx,也可以在网关层统一添加响应头,这样 Java 服务自身不必改动。下面是一段常见的 Nginx 配置:
location /api/ {
if ($request_method = OPTIONS) {
add_header Access-Control-Allow-Origin http://localhost:5173;
add_header Access-Control-Allow-Methods GET,POST,PUT,DELETE,OPTIONS;
add_header Access-Control-Allow-Headers Content-Type,Authorization;
add_header Access-Control-Max-Age 3600;
return 204;
}
proxy_pass http://backend_server;
add_header Access-Control-Allow-Origin http://localhost:5173;
add_header Access-Control-Allow-Methods GET,POST,PUT,DELETE,OPTIONS;
add_header Access-Control-Allow-Headers Content-Type,Authorization;
}
Nginx 的 if 指令只适合简单判断,如果有多环境、多域名来源,更稳妥的做法是用 map 动态设置 Access-Control-Allow-Origin。还需注意 add_header 的继承规则,如果内部 location 重新定义了 add_header,外层配置可能不生效。
四、配置中的常见坑与排查方法
CORS 配置看似简单,但实际排查时很容易被细节卡住。最常见的是配置了 allowCredentials(true),却把 allowedOrigins 写成星号。浏览器会认为这种组合不可信,CORS 校验仍然失败。Spring 5.3 之后提供了 allowedOriginPatterns,可以写成 allowedOriginPatterns("*") 配合 allowCredentials(true),框架会回显具体来源而不是返回星号。
另一个高频问题是只返回了 Access-Control-Allow-Origin,却漏掉 Access-Control-Allow-Headers。比如前端在请求头里带了 Authorization 或 X-Requested-With,预检请求会问服务器是否允许这些头。如果后端只配置了允许 Content-Type,预检同样失败。解决方法是先看浏览器 Network 面板里的 OPTIONS 请求,查看 Requested Headers 和响应头是否一一对应。
如果使用了自定义响应头,比如前端要读取 Content-Disposition 或 X-Total-Count,Java 后端还需要额外配置 Access-Control-Expose-Headers。否则浏览器虽然能收到响应,但 JavaScript 无法读取这些头的值,这种问题容易被误判为接口没返回数据。
排查时可以优先检查三件事:第一,请求是否为预检请求,OPTIONS 是否被安全框架拦截;第二,响应头里的 Origin 是否与当前页面来源完全一致,包括协议、域名、端口;第三,是否同时开启了凭证却没有使用具体来源。按照这个顺序检查,大多数 CORS 问题能在几分钟内定位。