跨域处理

CORS(Cross-Origin Resource Sharing,跨源资源共享)是浏览器的安全机制,当前端与后端的 协议 / 域名 / 端口 任一不同时,浏览器会拦截响应。

CORS 工作原理

简单请求(GET / POST + 普通 Content-Type):
  浏览器                          服务器
    │  GET /api/data               │
    │  Origin: http://localhost:3000 →
    │                              │ 200 OK
    │  ← Access-Control-Allow-Origin: http://localhost:3000
    │  (缺少该头 → 浏览器拦截响应)

预检请求(PUT / DELETE / 自定义 Header / JSON body 等):
  浏览器                          服务器
    │  OPTIONS /api/data           │
    │  Origin: http://localhost:3000
    │  Access-Control-Request-Method: PUT →
    │                              │ 204 No Content
    │  ← Access-Control-Allow-Origin: ...
    │  ← Access-Control-Allow-Methods: GET,POST,PUT
    │  ← Access-Control-Max-Age: 3600
    │
    │  PUT /api/data(实际请求)    │
    │  Origin: http://localhost:3000 →

CORS 是浏览器行为,Postman / curl 等工具不受影响。

方案一:@CrossOrigin(方法 / 类级别)

适合只有少数接口需要跨域的场景:

@RestController
@RequestMapping("/api/users")
// 类级别:该 Controller 下所有接口都允许跨域
@CrossOrigin(origins = "http://localhost:3000")
public class UserController {
 
    // 方法级别:覆盖类级别配置
    @CrossOrigin(
        origins = {"http://localhost:3000", "https://app.example.com"},
        methods = {RequestMethod.GET, RequestMethod.POST},
        allowedHeaders = {"Authorization", "Content-Type"},
        maxAge = 3600
    )
    @GetMapping("/{id}")
    public User getUser(@PathVariable Long id) { ... }
}

方案二:全局 CORS 配置(推荐)

通过 WebMvcConfigurer 统一配置,适合大多数前后端分离项目:

@Configuration
public class CorsConfig implements WebMvcConfigurer {
 
    @Override
    public void addCorsMappings(CorsRegistry registry) {
        registry.addMapping("/api/**")               // 匹配路径
            .allowedOrigins(
                "http://localhost:3000",             // 开发环境
                "https://app.example.com"            // 生产环境
            )
            .allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS", "PATCH")
            .allowedHeaders("*")                     // 允许所有请求头
            .exposedHeaders("X-Total-Count")         // 暴露给前端的响应头
            .allowCredentials(true)                  // 允许携带 Cookie
            .maxAge(3600);                           // 预检缓存时长(秒)
    }
}

allowCredentials(true)allowedOrigins("*") 不能同时使用,需指定具体域名。

多环境域名配置

@Configuration
@RequiredArgsConstructor
public class CorsConfig implements WebMvcConfigurer {
 
    @Value("${cors.allowed-origins}")
    private String[] allowedOrigins;
 
    @Override
    public void addCorsMappings(CorsRegistry registry) {
        registry.addMapping("/**")
            .allowedOrigins(allowedOrigins)
            .allowedMethods("*")
            .allowedHeaders("*")
            .allowCredentials(true)
            .maxAge(3600);
    }
}
# application-dev.yml
cors:
  allowed-origins:
    - http://localhost:3000
    - http://localhost:5173
 
# application-prod.yml
cors:
  allowed-origins:
    - https://app.example.com

环境与 Profile 详见 环境与Profile

方案三:CorsFilter(与 Spring Security 配合)

WebMvcConfigurer 的跨域配置在 Spring Security 过滤器之后执行,当 Security 拦截了预检请求(OPTIONS)时会导致失效。正确做法是注入 CorsFilter

@Configuration
public class CorsConfig {
 
    @Bean
    public CorsFilter corsFilter() {
        CorsConfiguration config = new CorsConfiguration();
        config.setAllowedOrigins(List.of(
            "http://localhost:3000",
            "https://app.example.com"
        ));
        config.setAllowedMethods(List.of("GET", "POST", "PUT", "DELETE", "OPTIONS", "PATCH"));
        config.setAllowedHeaders(List.of("*"));
        config.setExposedHeaders(List.of("Authorization", "X-Total-Count"));
        config.setAllowCredentials(true);
        config.setMaxAge(3600L);
 
        UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
        source.registerCorsConfiguration("/**", config);
 
        return new CorsFilter(source);
    }
}

Spring Security 中开启 CORS

@Configuration
@EnableWebSecurity
public class SecurityConfig {
 
    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        http
            .cors(cors -> cors.configurationSource(corsConfigurationSource()))  // 启用 CORS
            .csrf(csrf -> csrf.disable())
            .authorizeHttpRequests(auth -> auth
                .requestMatchers(HttpMethod.OPTIONS, "/**").permitAll()  // 预检请求直接放行
                .anyRequest().authenticated()
            );
        return http.build();
    }
 
    @Bean
    public CorsConfigurationSource corsConfigurationSource() {
        CorsConfiguration config = new CorsConfiguration();
        config.setAllowedOrigins(List.of("http://localhost:3000"));
        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;
    }
}

Spring Security 详见 安全

三种方案选型

场景推荐方案
个别接口跨域@CrossOrigin
全局跨域,不用 Spring SecurityWebMvcConfigurer.addCorsMappings
全局跨域 + Spring SecurityCorsFilter Bean 或 http.cors()

常见问题

1. 预检请求被 401 拦截

原因:Spring Security 对 OPTIONS 请求进行了认证检查。

// 修复:显式放行所有 OPTIONS 请求
.requestMatchers(HttpMethod.OPTIONS, "/**").permitAll()
// 或:在 CorsFilter 中统一处理,优先级高于 Security

原因:allowCredentials 未设置,或前端未开启 withCredentials

// 后端
config.setAllowCredentials(true);
config.setAllowedOrigins(List.of("http://localhost:3000")); // 不能用 *
 
// 前端 axios
axios.defaults.withCredentials = true;

3. 自定义响应头前端无法读取

默认情况下,浏览器只暴露部分标准响应头,自定义头需显式暴露:

config.setExposedHeaders(List.of("X-Total-Count", "X-Page-Number"));
// 前端才能读取到
const total = response.headers['x-total-count'];

4. 开发环境用代理替代 CORS

前端开发时,可通过 Vite / webpack-dev-server 代理转发请求,完全绕过 CORS:

// vite.config.ts
export default {
  server: {
    proxy: {
      '/api': {
        target: 'http://localhost:8080',
        changeOrigin: true
      }
    }
  }
}

此方案只适用于开发环境,生产环境仍需服务端正确配置 CORS。

相关链接