증상 — 에러 메시지
프론트엔드에서 API를 호출하면 다음과 같은 CORS 에러가 콘솔에 출력됩니다:
Access to XMLHttpRequest at 'https://api.example.com/api/v1/users' from origin 'https://app.example.com' has been blocked by CORS policy: Response to preflight request doesn't pass access control check: No 'Access-Control-Allow-Origin' header is present on the requested resource.
특히 @CrossOrigin 어노테이션이나 WebMvcConfigurer로 CORS를 설정했는데도 에러가 사라지지 않는 경우, Spring Security가 원인일 가능성이 높습니다.
원인
Spring Boot 프로젝트에 Spring Security가 추가되면 CORS 처리 순서가 달라집니다. Spring Security의 필터 체인이 Spring MVC의 CORS 처리보다 먼저 실행되기 때문에, @CrossOrigin 이나 WebMvcConfigurer를 통한 CORS 설정이 Security 레이어에서 막혀버립니다.
Preflight 요청(OPTIONS)은 인증 헤더가 없기 때문에 Spring Security가 401/403으로 거부하고, 브라우저는 CORS 에러로 인식합니다. 즉, CORS 설정이 잘못된 게 아니라 Security 필터가 CORS 처리 전에 먼저 요청을 차단하는 것이 핵심 원인입니다.
해결방법
방법 1: SecurityFilterChain에 CORS 직접 등록 (Spring Boot 3.x 권장)
@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("/api/public/**").permitAll()
.anyRequest().authenticated()
);
return http.build();
}
@Bean
public CorsConfigurationSource corsConfigurationSource() {
CorsConfiguration config = new CorsConfiguration();
config.setAllowedOrigins(List.of("https://app.example.com"));
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;
}
}
방법 2: WebMvcConfigurer와 Security 통합
@Configuration
public class WebConfig implements WebMvcConfigurer {
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/**")
.allowedOrigins("https://app.example.com")
.allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS")
.allowedHeaders("*")
.allowCredentials(true)
.maxAge(3600);
}
}
// Security 설정에서 반드시 cors() 활성화
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.cors(Customizer.withDefaults()) // WebMvcConfigurer 설정 사용
.csrf(csrf -> csrf.disable())
// ...
return http.build();
}
자주 하는 실수 3가지
1. Security 설정에 .cors() 없이 WebMvcConfigurer만 설정 — Security 필터가 OPTIONS 요청을 차단함
2. allowedOrigins에 trailing slash 포함 — "https://app.example.com/" 처럼 끝에 슬래시 금지
3. allowedMethods에 OPTIONS 누락 — Preflight 통과를 위해 OPTIONS 반드시 포함
원인별 정리표
| 증상 | 원인 | 해결 |
|---|---|---|
| OPTIONS 요청 401/403 | Security가 Preflight 차단 | cors() 활성화 + permitAll |
| @CrossOrigin 무시됨 | Security 필터 우선 실행 | SecurityFilterChain에 cors() 추가 |
| 쿠키/세션 전송 안 됨 | allowCredentials 미설정 | setAllowCredentials(true) |
| 특정 헤더만 에러 | allowedHeaders 미포함 | setAllowedHeaders(List.of("*")) |
| 로컬에서만 동작 | origin 목록에 배포 URL 없음 | allowedOrigins에 배포 도메인 추가 |
핵심 정리
Spring Security + CORS 문제의 90%는 SecurityFilterChain에 .cors()를 추가하지 않아서 발생합니다. @CrossOrigin이나 WebMvcConfigurer만으로는 Security 레이어를 통과할 수 없습니다. Spring Boot 3.x 환경에서는 corsConfigurationSource Bean을 별도로 등록하는 방법 1이 가장 명확하고 안정적입니다.
댓글 없음:
댓글 쓰기