교차출처 리소스 공유[Cross-Origin Resource Sharing, CORS] 이슈 간단 설명 및 해결 (Filter, Spring Security, CorsConfigurationSource)

현재 Rest API 개발과 함께 풀스택을 지향하며 Vuejs를 학습하며 서버와 클라이언트를 모두 학습중입니다. 

 

직장에서는 서버 API 개발을 하면서 실무에서 배워가면 학습을 하고 있는 편입니다.

 

그런데 API를 먼저 TDD를 통한 개발방법, Postman으로 테스트해보면서 Vue 프레임워크를 이용해서 프론트를 입히는 작업을 하는 도중 한가지 이슈에 봉착하게 되었습니다.

 

바로 교차출처 리소스 공유인데요. 흔히 CORS라고 축약되어 부르는 단어입니다.

 

이는 기존에 많은 SI에서 채택한 Spring, JSP를 이용한 개발에서는 확인하기 힘든 이슈였습니다.

 

이유는 서버사이드 렌더링으로 모든 페이지들을 불러오고 필요한 데이터 호출은 Ajax로 처리하던 기존의 방식은 서버와 클라이언트의 출처(Resource)가 한 프로젝트 내에서 이루어지기 때문입니다.

 

하지만 제가 학습하고 있는 REST API(db포함) + vue.js 클라이언트를 이용한 개발에서는 API와 Web Application이 서로 출처가 다른 상태에서 http 통신을 하기 때문입니다. ( localhost:8080과 localhost:3000은 서로 다른 출처입니다. )

 

cors 이슈는 간단히 말하면 무분별한 http 통신을 제한하기 위해서 같은 출처 혹은 특정 url에 한해서만(설정필요) 통신을 허용하도록 하는 것입니다.

 

자세한 내용을 해당 내용을 참조해보시면 좋을 것 같습니다. https://developer.mozilla.org/ko/docs/Web/HTTP/CORS

 

교차 출처 리소스 공유 (CORS)

교차 출처 리소스 공유(Cross-Origin Resource Sharing, CORS)는 추가 HTTP 헤더를 사용하여, 한 출처에서 실행 중인 웹 애플리케이션이 다른 출처의 선택한 자원에 접근할 수 있는 권한을 부여하도록 브라우저에 알려주는 체제입니다. 웹 애플리케이션은 리소스가 자신의 출처(도메인, 프로토콜, 포트)와 다를 때 교차 출처 HTTP 요청을 실행합니다.

developer.mozilla.org

 

일단 cors 이슈를 해결하기 전,  위에 있는 문서를 정독한 후 한가지 사실을 알아냈습니다.

 

"브라우저는 Cors 이슈에 대해서 api에 대해 http 통신을 하기 전 프리플라이트(preflight, 사전전달) 작업을 거친다."

 

프리플라이트는 브라우저가 현재 웹 어플리케이션과 호출한 api의 Resource 출처가 동일한지를 먼저 http Options 메소드로 통신을 시도해보는 것입니다.

 

여기서 통과가 되면 Same Resource로 http 통신이 허가가 되는 것입니다.

 

통과되기 위해서는 특정 헤더를 포함해야하는데 이는 아래의 소스를 보며 확인해봅시다.

 

@WebFilter(urlPatterns = {"/api/**"}, description = "API 필터")
@Component("CorsFilter")
@Slf4j
public class CORSFilter implements Filter {
    @Override
    public void init(FilterConfig filterConfig) throws ServletException {

    }

    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) 
    								throws IOException, ServletException {
        HttpServletRequest httpServletRequest = (HttpServletRequest) request;
        HttpServletResponse httpServletResponse = (HttpServletResponse) response;

        httpServletRequest.setCharacterEncoding("utf-8");
        //set header
        
        httpServletResponse.setHeader("Access-Control-Allow-Origin", "*");
        httpServletResponse.setHeader("Access-Control-Allow-Methods", 
        							  "POST, GET, DELETE, PUT, PATCH, OPTIONS");
        httpServletResponse.setHeader("Accept-Charset", "utf-8");
        httpServletResponse.setHeader("Cache-Control", "no-cache");
        httpServletResponse.setHeader("Expires", "-1");
        httpServletResponse.setHeader("Pragma", "no-cache");

        chain.doFilter(httpServletRequest, httpServletResponse);
    }

    @Override
    public void destroy() {

    }
}

간단히 소스를 설명드리자면 API에 Filter를 걸어 @WebFilter에서 urlPatterns에 해당하는 uri가 호출되는 경우 filter를 수행하여 response에 cors 이슈를 해결할 header들을 넣어줍니다. 

- Access-Control-Allow-Origin : 허용할 출처

- Access-Control-Allow-Methods : 허용할 HTTP 메소드

크게는 이 두가지가 필수요소입니다.

 

이렇게 api에 필터를 걸게되면 반환되는 response는 브라우저가 위 헤더들을 보며 같은 출처의 리소스라고 판단하며 http 통신을 할 수 있도록 preflight를 승인해주게 됩니다.

 

하지만

 

Spring Security를 이용하는 경우...!

spring security를 적용중이라면 servlet filter로 cors를 적용한다면 원치않는 결과를 얻을 수 있습니다.

 

즉, 원하는 방향으로 동작하지 않을 수 있다는 것이죠.

 

일단 spring security를 이용하는 경우 CorsConfigurationSource 빈을 활용하여 처리하였습니다.

 

@Bean
public CorsConfigurationSource corsConfigurationSource(){
  System.out.println("----------------cors config-----------------------");

  CorsConfiguration configuration = new CorsConfiguration();

  configuration.addAllowedOrigin("*");
  configuration.addAllowedMethod("*");
  configuration.addAllowedHeader("*");
  configuration.setAllowCredentials(true);
  configuration.setMaxAge(3600L);
  UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
  source.registerCorsConfiguration("/**", configuration);

  System.out.println("----------------cors config end-----------------------");
  return source;
}

그 전에 configure 에서 아래와 같은 설정들이 필요합니다.

 

 http
                .cors() //필수
                .and()
                .csrf()
                .disable()
                .authorizeRequests()
                //preflight는 인증하지 않아도된다.
                .requestMatchers(CorsUtils::isPreFlightRequest).permitAll() 
                .antMatchers("/", "/hello").permitAll().antMatchers("/my").authenticated();

브라우저의 preflight는 인증없이 options 메소드로 헤더를 확인하여 통신할 수 있도록 해주는 설정이 필요합니다.

 

 

댓글

Designed by JB FACTORY