Spring Security의 Prefligt Options 요청 처리 에러 - TaeDongUm/Step_i GitHub Wiki
✅ Preflight이란?
- **CORS(Cross-Origin Resource Sharing)**와 관련된 개념
- 브라우저가 보안상의 이유로 교차 출처 요청을 보낼 때 **실제 요청을 보내기 전에 서버에 허락을 구하는 사전 요청(preflight request)**임
✅ Preflight 요청이 발생하는 조건
다음 중 하나라도 해당되면 브라우저는 OPTIONS 메서드를 사용해 preflight 요청을 먼저 보냄:
- Content-Type이 application/json, text/plain, multipart/form-data 이외의 경우
- Authorization 헤더, X-Auth-Token 같은 커스텀 헤더가 포함된 경우
- PUT, DELETE, PATCH 같은 "simple method"가 아닌 HTTP 메서드 사용
- CORS 요청이 자격 증명(credential)을 포함하는 경우 (withCredentials: true)
🛫 Preflight 흐름 예시
OPTIONS /api/data HTTP/1.1
Origin: https://my-frontend.com
Access-Control-Request-Method: POST
Access-Control-Request-Headers: Content-Type, Authorization
이 요청에 대해 서버가 허용하면 아래와 같은 응답을 보냄:
HTTP/1.1 204 No Content
Access-Control-Allow-Origin: https://my-frontend.com
Access-Control-Allow-Methods: POST, GET, OPTIONS
Access-Control-Allow-Headers: Content-Type, Authorization
이후 브라우저가 본 요청을 보냄.
🚨 문제 핵심
- Spring Security가 Preflight 요청(OPTIONS)을 인증이 필요한 요청으로 판단하여 401 Unauthorized 응답을 반환하고 있었음.
- 이는 Spring Security의 기본 인증 설정(Basic Auth)이 활성화되어 있었기 때문.
🛠️ 해결 방법
- OPTIONS 요청 허용: Spring Security에서 OPTIONS 요청을 명시적으로 허용하여 인증 없이 처리되도록 설정.
- Basic Auth 비활성화: httpBasic(http -> http.disable())를 사용하여 기본 인증을 완전히 비활성화.
package com.stepi.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpMethod;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.web.SecurityFilterChain;
@Configuration
@EnableWebSecurity
public class SecurityConfig {
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
.csrf(csrf -> csrf.disable())
.authorizeHttpRequests(auth -> auth
.requestMatchers(HttpMethod.OPTIONS, "/**").permitAll() // OPTIONS 요청 허용
.requestMatchers("/api/auth/**").permitAll()
.anyRequest().authenticated()
)
.httpBasic(http -> http.disable()) // Basic 인증 비활성화
.formLogin(form -> form.disable())
.logout(logout -> logout.disable())
.headers(headers -> headers
.frameOptions(frame -> frame.disable())
.xssProtection(xss -> xss.disable())
);
return http.build();
}
}
- CORS 설정 적용: CorsFilter를 Security FilterChain보다 먼저 실행되도록 설정하여 CORS 헤더가 응답에 포함되도록 수정.
package com.stepi.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.CorsConfigurationSource;
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
import org.springframework.web.filter.CorsFilter;
import java.util.List;
@Configuration
public class CorsConfig {
@Bean
public CorsConfigurationSource corsConfigurationSource() {
CorsConfiguration config = new CorsConfiguration();
config.setAllowedOrigins(List.of("http://localhost:5173")); // 정확한 Origin 설정
config.setAllowedMethods(List.of("GET", "POST", "PUT", "DELETE", "OPTIONS", "PATCH"));
config.setAllowedHeaders(List.of(
"Authorization",
"Content-Type",
"X-Requested-With",
"Accept",
"Origin",
"Access-Control-Request-Method",
"Access-Control-Request-Headers"
));
config.setExposedHeaders(List.of("Authorization"));
config.setAllowCredentials(true); // 인증 정보 허용
config.setMaxAge(3600L);
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/**", config);
return source;
}
@Bean
public CorsFilter corsFilter() {
return new CorsFilter(corsConfigurationSource()); // Security FilterChain보다 먼저 실행
}
}