[Spring] 스프링 시큐리티로 CORS와 preflight 설정하기
스프링 시큐리티로 CORS 설정 삽질해서 해결한 과정을 올려본다
사실 결론부터 말하자면 설정은 몇 줄 빼고 아주 잘 되어있었고
도커 이미지 태그가 달라져서 갱신이 안됐던거였다 ^^^^^;;;
로컬 환경에서는 너무나도 잘 되다가 EC2에 띄웠을 때만
자꾸 계속 preflight 과정에서 CORS 이슈가 생겨서 무한 삽질을 하고 있었다
그러다가 도커 컴포즈 파일을 봤는데
도커 이미지 태그가 바뀐 걸 보고 아,,, 한숨밖에 안나왔다
그 과정에서 CORS 관련 공부는 된 거 같아서 긍정적으로 생각할란다
🔥 문제 상황
주니어 개발자들을 벌벌 떨게 만드는 무시무시한 CORS 에러
Access to XMLHttpRequest at '{SERVER}' from origin '{ANOTHER ORIGIN}' 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.
특히 Postman 으로 API 요청을 보냈을 때는 매우 잘되는데
막상 실제 리액트 앱을 만들어 요청했을 때 이 에러가 발생하는 경우가 있다
나도 그랬다
이것도 브라우저에서 보내는 Preflight 요청 때문이었다 (후술)
📌 CORS ?
Cross Origin Resource Sharing 의 약자
하나의 도메인에서 다른 도메인의 리소스에 접근할 수 있게 해주는 보안 메커니즘
동일 출처 정책(SOP), 즉 프로토콜과 호스트명, 포트가 같은 출처의 리소스에만 접근할 수 있도록 제한하는 정책 때문에 등장했다.
서버 사이드 렌더링으로 자원을 뿌려줄 때는 신경쓸 일이 거의 없지만
요즘에는 API 서버와 클라이언트(리액트나 뷰)를 분리하기 때문에 자주 발생하는 에러이다,,,,
CORS 에러를 해결하기 위해서는
API 서버에서 클라이언트 URL에 대한 리소스 참고를 허용하도록
헤더에 "Access-Control-Allow-Origin" 값을 넣어 응답을 해줘야 한다.
하지만 이 헤더를 보내 해결할 수 없는 경우가 몇 가지 있는데,
- GET, POST, HEAD 를 제외한 메소드를 사용
- Accept, Accept-Language, Content-Language, Content-Type, DPR, Downlink,
Save-Data, Viewport-Width, Width 를 제외한 헤더를 사용 - Content-Type 에서 application/x-www-form-urlencoded, mulitpart/form-data, text/plain 이외의 것을 사용
특히 2번과 3번에서 문제가 되는데
JWT 방식으로 로그인을 구현할 때 X-AUTH-TOKEN 또는 Authorization 헤더를 사용하고
Content-Type 으로 application/json 을 보내는 경우가 많기 때문이다
📌 Preflight
"Access-Control-Allow-Origin" 헤더로 해결할 수 없는 경우
사이트가 안전한지 확인하기 위해 간을 보는데
예비 요청으로 OPTIONS 메서드를 먼저 보내서 판단한 후 본 요청을 보내게 된다
이것이 바로 preflight 요청이다
아까 만났던 에러는 바로 이 preflight 요청을 서버에서 처리하지 못해서
내 요청이 팅겨져 나간 것이다
그러니까 결론적으로는 서버에서 preflight 요청을 처리할 수 있도록
OPTIONS 메서드를 허용해주어야 한다.
📌 스프링 시큐리티 설정
📝 SecurityConfiguration.java
@EnableWebSecurity
@RequiredArgsConstructor
@Configuration
public class SecurityConfiguration {
private final JwtAuthenticationEntryPoint jwtAuthenticationEntryPoint;
private final JwtAuthenticationFilter jwtAuthenticationFilter;
private final JwtAccessDeniedHandler jwtAccessDeniedHandler;
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.cors().configurationSource(corsConfigurationSource())
.and()
.csrf().disable()
//예외처리 핸들러
.exceptionHandling()
.authenticationEntryPoint(jwtAuthenticationEntryPoint)
.accessDeniedHandler(jwtAccessDeniedHandler)
.and()
.sessionManagement()
.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
.httpBasic().disable()
//권한이 필요한 요청에 대한 설정
.authorizeRequests()
.requestMatchers(CorsUtils::isPreFlightRequest).permitAll()
.antMatchers("/admin/**").hasAuthority("ROLE_ADMIN")
.antMatchers("/user/**").authenticated()
.anyRequest().permitAll()
.and()
.headers().frameOptions().disable();
http.addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class);
return http.build();
}
/** cors 설정 bean */
@Bean
public CorsConfigurationSource corsConfigurationSource() {
CorsConfiguration configuration = new CorsConfiguration();
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
configuration.addAllowedOriginPattern("*");
configuration.addAllowedHeader("*");
configuration.addAllowedMethod("*");
configuration.addExposedHeader("x-auth-token");
configuration.setAllowCredentials(true);
configuration.setMaxAge(3600L);
source.registerCorsConfiguration("/**", configuration);
return source;
}
}
스프링 MVC 말고 스프링 시큐리티를 사용중이라면
CORS 관련 설정을 전역적으로 할 수 있다
간혹 Controller 단에서 @CrossOrigin 에노테이션 박아서 사용하는 코드를 많이 보는데
별로 좋은 방법은 아니라고 생각한다
여기서 핵심은 시큐리티 필터의 허용 Request 에 PreFlight 요청을 추가하는 것이다
...
.authorizeRequests()
.requestMatchers(CorsUtils::isPreFlightRequest).permitAll()
...
나는 CorsUtils 클래스에서 가져왔는데
HttpMethod.OPTIONS 로 빼서 가져와도 된다
이후 CorsConfigurationSource 빈에서 CORS 관련 설정을 해주면 끝이다
...
configuration.addAllowedOriginPattern("*");
configuration.addAllowedHeader("*");
configuration.addAllowedMethod("*");
configuration.addExposedHeader("x-auth-token");
configuration.setAllowCredentials(true);
configuration.setMaxAge(3600L);
...
스프링 최신 버전에서는
addAllowedOrigin 과 setAllowCredentials(true)를 같이 사용할 수 없다고 오류가 뜬다
에러 메세지 그대로 addAllowedOriginPattern 으로 수정해주면 된다
배포 이후 프론트단 리액트앱에서 axios 요청 날리니까 정상적으로 되는 모습
서버 도커 이미지 제대로 풀 해오니까 잘 되더라
이상 스프링 시큐리티 CORS 와 Preflight 관련한 삽질의 흔적이었습니다