Search

CORS 설정 안되는 이슈

태그
팀 프로젝트
Spring
작성 상태
작성 완료
작성일
2022/05/31
참고 링크
참고 링크 2

개요 - CORS 설정이 안먹힌다고..?

이번에 팀 프로젝트로 진행중인 골키퍼 프로젝트에서 프론트엔드 개발자분들이 CORS를 해결해달라고 요청하셔서, 스프링 설정파일에 CORS 예외 설정을 추가했다. 아래 설정 클래스의 9 ~ 13 라인이 CORS 예외 설정을 하는 코드다.
package com.j2kb.goal.config; //import 문 생략@Configuration @EnableTransactionManagement public class WebConfig implements WebMvcConfigurer { @Autowired private DataSource dataSource; @Override public void addCorsMappings(CorsRegistry registry) { registry.addMapping("/**") .allowedOriginPatterns("*"); } @Override public void addInterceptors(InterceptorRegistry registry) { registry.addInterceptor(new MemberCertInterceptor()) .addPathPatterns("/api/**")// 해당 경로에 접근하기 전에 인터셉터가 가로챈다. .excludePathPatterns("/api/members","/api/members/login","/api/admin","/api/statistics/total","/api/admin/**");// 해당 경로는 인터셉터가 가로채지 않는다. } @Bean public PlatformTransactionManager transactionManager(){ DataSourceTransactionManager tm = new DataSourceTransactionManager(); tm.setDataSource(dataSource); return tm; } }
Java
복사
예외 설정 코드는 Spring 공식 문서(Global CORS configuration)에서 참조한 것이라 분명히 정확히 동작해야 했다.
그런데 여전히 CORS 에러가 난다는프론트분들의 카톡..
CORS.. 삽질의 시작..
그 후에 별의 별 자료를 찾아가면서 CORS 예외 설정을 바꿔보았지만, 전혀 소용이 없었다. 그래서, 기본부터 시작하자는 의미로 CORS 체크의 원리에 대해 검색해 보았다.

CORS 검사의 원리

브라우저는, 서버에 HTTP 요청을 보낼 때 Origin 헤더에 요청을 보내는 출처를 담아보낸다. 아마, 프론트엔드 개발자들의 개발환경에서 브라우저를 이용해 요청을 보낼 때는
Origin : http://localhost:3000
CSS
복사
이런 헤더가 포함될 것이다.
브라우저로부터 요청을 받은 서버는 응답을 보내는데, 이 응답에 포함된 Access-Control-Allow-Origin 헤더에 "서버가 제공하는 자원에 접근 가능한 출처 목록"을 포함시켜 응답한다.
Access-Control-Allow-Origin : https://example.com
CSS
복사
브라우저가 서버로부터 응답을 받으면, 자신이 보냈던 요청의 Origin과 서버가 보내준 응답의 Access-Control-Allow-Origin 를 비교해 유효하면 응답을 처리하고, 유효하지 않다면 에러를 발생시킨다.
CORS 에러
CORS를 탐지하는 원리 자체는 간단하다. 참고로, 브라우저에서 서버로 요청을 보내는 시나리오가 3가지 정도가 있는데, 그 3가지 시나리오중 어떤 시나리오에서 CORS 에러가 발생하는지 알면 대응이 쉬워진다. 이번에 문제가 일어났던 시나리오에서만

Preflight Request

가장 일반적인 상황으로 "브라우저가 본 요청을 보내기 전에 예비요청을 보내고, 예비요청의 결과에 따라 본요청을 보내는 방식" 이다. 요청 플로우는 아래 그림과 같다.
preflighted 요청 플로우 출처 :https://developer.mozilla.org/ko/docs/Web/HTTP/CORS
OPTIONS 메서드로 예비요청을 보내고, 그 응답이 정상이면(상태코드 기반이 아니라, 응답의 헤더로 넘어오는 여러가지 값들을 보고 판단한다) 보다 구체적인 것은 여기를 참고하자.
이래서, POSTMAN 으로 Origin을 설정하여 테스트 했을때는 CORS 가 발생하지 않고 정상적으로 응답을 해 주었지만, 브라우저를 이용했을 때는 CORS 가 발생한 것이다. 백엔드 코드에선 OPTIONS 요청을 전혀 고려하지 않았고, 이것이 제대로 처리되지 않을경우 예비요청에 제대로 응답할 수 없어서 본요청이 전송조차 되지 않는 것이다.

OPTIONS, 너 어디서 잘못 처리되고 있는거니?

분명한 것은 CORS 설정에서
@Override public void addCorsMappings(CorsRegistry registry) { registry.addMapping("/**") .allowedOriginPatterns("*"); }
Java
복사
로 모든 출처에서의 요청을 허용해두었기 때문에(물론, 현재 테스트용으로 이렇게 해둔 것이고, 실제 서비스용도에서 이렇게 해두면 절대 안된다)  Origin 값은 문제가 되지 않는다. 스프링 공식문서를 찾아보니, 특정 HTTP 메서드에 대해 CORS를 발생하지 않게 하는게 있어서 그것을 이용해보았다.
@Override public void addCorsMappings(CorsRegistry registry) { registry.addMapping("/**") .allowedOriginPatterns("*") .allowedMethods("*"); }
Java
복사
하지만, 불행하게도 여전히 CORS는 발생했다. 여기서 알 수 있는 사실은
"CORS 설정 이외에도 모든 요청에 공통적으로 관여하는 뭔가가 CORS 처리를 방해했다." 라는 것이다. 아! 인터셉터!

Intercepter 네이놈!

나는 이 시스템에서 인증을 Spring Intercepter를 이용해 처리하고 있었다. 이 인터셉터를 구현할 때 나는 preflighted 요청을 전혀 몰랐고, 따라서 관련된 처리를 전혀 하지 않았다.
Preflighted 요청중 예비요청인 OPTIONS 요청은 브라우저가 자동으로 보낸다.
따라서, "인증토큰이 헤더에 포함될리가 없다" 그런데, 내가 로그인 처리를 위한 인터셉터에서 이렇게 구현해두었다.
@Component public class MemberCertInterceptor implements HandlerInterceptor { @Autowired private MemberRepository memberRepository; @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { String auth = request.getHeader("Authorization"); if(JwtBuilder.isValid(auth)){ return true; }else{ return false; } } @Override public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, @Nullable ModelAndView modelAndView) throws Exception { } }
Java
복사
8번째 줄을 보면, 요청의 헤더로부터 인증토큰을 추출하려고 시도한다. 당연히 OPTIONS 요청엔 이게 포함되지 않고,
auth 에는 null이 들어가게 된다. 따라서 그 아래의 코드에서 핸들링되지 않은 NPE가 발생해서 정상적인 응답을 해주지 못했을 것이다(핸들링되지 않은 예외라 스프링이 처리하는데, 아마 500 에러를 반환하지 않았을까 한다). 따라서, CORS 설정이 제대로 동작하지 않았던 것. 이를 해결하기 위해 다음 코드를 추가하여 OPTIONS의 경우 토큰검사를 진행하지 않게 했다.
@Component public class MemberCertInterceptor implements HandlerInterceptor { @Autowired private MemberRepository memberRepository; @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { // 추가된 코드if(HttpMethod.OPTIONS.matches(request.getMethod())){ return true; } // 추가된 코드 String auth = request.getHeader("Authorization"); if(JwtBuilder.isValid(auth)){ return true; }else{ return false; } } @Override public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, @Nullable ModelAndView modelAndView) throws Exception { } }
Java
복사
이렇게 문제가 해결되었다.
하지만, 한가지 의문점이 남았다.
인터셉터에서 NPE가 발생했을때 적절히 핸들링하여 false를 반환했다면 어떻게 되었을까?
다음 포스팅에서 한번 테스트 해봐야겠다.