[Troubleshooting] - CORS 탐방기

청주는사과아님·2024년 11월 10일
0

Troubleshooting

목록 보기
3/7

프로젝트를 진행하던 중, frontendproxy 설정에 따라 backend CORS 설정이 달라지는 상황을 맞이하였습니다.

본래 CORS 를 말로만 들어봤고 실제 접해본 건 처음이라, 이를 정리하고 설명하는 포스팅을 적고자 합니다.


📝 상황 설명

저희는 프로젝트를 진행하며 시연 영상을 위해 frontend 를 구현하였고, 아래처럼 Proxy 설정을 덧붙였습니다.

// package.json
{
  "proxy" : "http://localhost:8080"
}

그런데 프로젝트 진행 중 갑자기 궁금중이 들어 위 proxy 를 해제해 보았고, 이전엔 나타나지 않던 CORS 가 발생하게 되었습니다.


❗️ 원인 분석

원인 분석을 위해 정확히 BrowserCORS 를 어떻게 처리하는지 알 필요성을 느꼈습니다.

그래서 이를 한번 정리해 보았습니다.


CORS 알아보기

CORS 는 서로 다른 출처를 가진 자원들로간 엑세스를 허용하거나 제한할 수 있도록 하는 HTTP 헤더 기반 메커니즘입니다.

이 때 출처를 확인하고 자원을 제한 하는 행동은 대게 Chrome 같은 브라우저가 담당합니다.

이를 우리가 개발하는 입장에서 설명하면 아래 그림과 같습니다.

브라우저는 Front 가 개발한 JavaScript, HTML 등을 토대로 동작합니다. 이 때 Front 의 코드에는 "Back 으로부터 ~~ 한 정보를 가져와라" 같은 코드가 존재할 것입니다.

그래서 브라우저는 Front 가 명시된 그대로 요청을 보내고 응답을 받습니다. 하지만 이 때 CORS 문제가 발생합니다.

브라우저가 "행동할 지침" 을 받은 자원은 Front, localhost:3000 으로부터 받은 자원입니다. 하지만 그 지침 속에는 Back, localhost:8080 으로부터 자원을 받는 것으로 명시되어 있습니다.

때문에 브라우저는 "내가 행동한 origin 은 3000 인데, 응답 받은 자원은 8080 이네? 위험한거 아냐??" 라고 생각해 CORS 를 일으킵니다.

즉, CORS브라우저가 자원간 출처를 인증하며 발생시키는 한 규약인 것입니다.

때문에 CORS 를 해결하기 위해선 "제공한 자원이 안전하다"브라우저에게 알려줘야 합니다.


Simple request, Preflight request

결국 브라우저는 안정성을 도모하기 위해 "제공한 자원이 안전" 한지 확인하며, 안전하지 못할 경우 CORS 를 일으킵니다.

브라우저가 "자원이 안전" 한지 확인하는 방식은 다양한데, 이 중 대표적인 2 방식을 설명하겠습니다.


I. Simple Request

Simple Request 는 브라우저가 확인할 요청이 단순해, "단 한 쌍의 요청 - 응답으로 안전한지 확인하는 방법" 이라 할 수 있습니다.

위 그림을 보면 Browser 가 요청의 Originlocalhost:3000 으로 인식하고, Server 로 부터 받은 응답의 Access-Controller-Allow-Origin 헤더가 localhost:3000 인지 확인하는 것을 볼 수 있습니다.

즉, 브라우저가 요청한 응답이 안전한지 확인하고 있습니다.

또한 위 그림을 보면 Browser, ServerRequest - Response 가 한 쌍만 존재하는 것을 볼 수 있습니다.

이 때 Simple Request 는 말 그대로 "단순한" 요청을 Server 로 보낼 시 이루어지는데, 자세한 요청 규칙은 다음과 같습니다.

  1. 요청 메서드는 GET, HEAD, POST 중 하나여야 한다.
  2. 요청 헤더에 오직 다음의 헤더만 사용되어야 한다.
    • Accept, Accept-Language, Content-Language, Content-Type, DPR, Downlink,Save-Data, Viewport-Width, Width
  3. Content-Type 헤더는 오직 다음 3 가지만 허용된다.
    • application/x-www-form-urlencoded, multipart/form-data, text/plain

때문에 요청 body 가 포함된 대부분의 요청은 Simple Request 대신, Preflight Request 를 통해 안정성을 검증합니다.


II. Preflight Request

Preflight Requestpreflight 뜻 그대로, "예비 요청으로 안전한지 먼저 확인하고, 본 요청을 보내는 방법" 이라 할 수 있습니다.

Preflight Request 는 크게 예비 요청본 요청 으로 나뉘는데, Browser예비 요청 을 통해 제공받 (본 요청의 Response) 자원이 안전한지 확인합니다.

이 때 주의할 점은 예비 요청 의 메서드는 HTTP OPTION 이며, "빈 요청" 을 보낸다는 점 입니다.

즉, 예비 요청body 는 비어있으며 HTTP OPTION 메서드로 보내집니다.

브라우저가 예비 요청 을 통해 안정성을 검사하면 정말로 필요한 본 요청 을 보낼지 판단합니다.

만약 안전하지 않으면 본 요청 은 이루어지지 않고, 안전하다면 요청이 이루어지게 됩니다.


FrontProxy

앞서 브라우저가 CORS 를 확인하는 방식 2 가지를 보았습니다. 이는 말 그대로 브라우저가 CORShandle 하는 방식이므로, Front 또는 Back 이 어떤 설정을 하더라도 이 방식 자체는 변하지 않습니다.

하지만 Front 에서 CORS 를 더 편하게 설정하는 방식이 있는데, 바로 Proxy 입니다.

Front 에서 Proxy 를 설정할 경우, 브라우저의 실제 요청은 Front 로 전달됩니다. 그리고 FrontBack 과 통신해 응답을 받고, 이를 다시 브라우저로 응답하게 됩니다.

즉, 브라우저 입장에서 FrontBack 인 것 처럼 (Proxy) 행동하는 것입니다.

이는 기존의 CORS handling 과 남다른 점을 보여줍니다.

Proxy 유무에 따라 브라우저 요청 응답을 제공하는 주인 (Host) 이 변하기 때문입니다.

Proxy 를 설정한 경우, (브라우저 입장에서) 모든 요청의 응답은 localhost:3000 으로 받습니다. 때문에 Back 에서 어떠한 CORS 설정을 하지 않아도 CORS 가 발생하지 않습니다.

더 자세히 설명하자면 FrontProxy 는 브라우저에게 "거짓말" 을 하는 것은 아닙니다.

Proxy 를 설정하면 Front 는 제공하는 자원 (JavaScript, HTML 등) 의 요청 URL Host 를 자기자신 (localhost:3000) 으로 설정합니다.

그래서 브라우저는 애초에 localhost:3000 으로 요청하는 코드를 받고 이를 실행할 뿐입니다.


✅ 문제 해결 방법

앞서 CORS 가 무엇인지, FrontProxy 에 따라 이것이 어떻게 변하는지 확인하였습니다.

이를 통해 생각해보면 프로젝트의 CORS 가 발생한 원인은 말하기 좀 부끄럽지만;; Back 에서 CORS 가 설정되지 않았기 때문이었습니다.

이를 해결하는 방법으로 2 가지를 생각하였습니다.


I. Front proxy 그대로 사용하기

첫째로, 너무나 당연하지만 프로젝트 초기 설정 그대로 FrontProxy 를 이용하는 방법입니다.

이는 package.jsonproxy 를 선언하거나 http-proxy-middleware 등의 proxy middleware 를 사용하는 방법이 있습니다.

// package.json
{
  "proxy" : "http://localhost:8080"
}
//src/setupProxy.js
const { createProxyMiddleware } = require("http-proxy-middleware");

module.exports = (app) => {
  app.use(
      "/api",
      createProxyMiddleware({
        target: "http://localhost:8080",
        changeOrigin: true,
      })
  );
};

이 방법은 개발을 진행하며 아주 간단히 CORS 를 해결할 수 있다는 장점이 있지만, 성능 overhead, Production code(조금) 부적합하다는 단점이 있습니다.

자세한건 front 라 몰루...


II. Back 에 CORS 설정하기

반면 BackCORS 를 그대로 설정하는 방법이 존재하는데, 자세한 구현은 "CORS 확인이 언제 이루어져야 하는가" 에 따라 달라집니다.

첫번째로 간단히 Spring MVC 구조에 CORS 를 설정하는 방법입니다.

// Security 없이 그냥 가능하다면
// **즉, Controller 단 까지 요청이 들어와 CORS 처리가 가능하다면**
@RestController
public class MainController {

   // Preflight Request 의 경우 OPTION 으로 예비 요청 보내는거 주의
   @CrossOrigin(
           methods = {RequestMethod.GET, RequestMethod.POST, RequestMethod.PUT,
                   RequestMethod.DELETE, RequestMethod.OPTIONS},
           origins = {"http://localhost:3000"},
           allowedHeaders = {"*"}
   )
   @RequestMapping
   public ResponseEntity<?> corsExample() {
      return null;
   }
   
}

// Global CORS config
// **요청이 MVC 구조까지 들어올 수 있다면**
@Configuration
@EnableWebMvc
public class WebConfig implements WebMvcConfigurer {

   @Override
   public void addCorsMappings(CorsRegistry registry) {

      registry.addMapping("/api/**")
              .allowedOrigins("https://domain2.com")
              .allowedMethods("PUT", "DELETE")
              .allowedHeaders("header1", "header2", "header3")
              .exposedHeaders("header1", "header2")
              .allowCredentials(true).maxAge(3600);
   }
}

@CrossOrigin, WebMvcConfigurerSpring MVC 에 포함되어 일반적인 MVC CORS 를 설정할 수 있습니다.

이 때 매우 중요한 사실이 있는데, 위 방법은 CORS 요청이 MVC 구조까지 들어와야 유효하다는 사실입니다.

저희 프로젝트의 경우 Spring Security 를 이용해 Filter 단 에서 여러 로직이 수행되었습니다.

특히 로그인 의 경우 LoginFilter 를 정의해 사용했는데, 이 때 로그인하는 Endpoint 또한 브라우저의 CORS 대상이 되었습니다.

문제는 LoginFilter 자체는 HTTP Method 를 구분하지 않는다는 점입니다.
브라우저는 로그인 EndpointPreflight 를 보냈고, 빈 Request body 때문에 Filter 내 에러가 발생해 CORS 가 발생하였습니다.

때문에 저희 프로젝트처럼 Filter 에 다양한 로직이 존재할 경우, @CrossOriginWebMvcConfigurer 대신 CorsFilter 를 정의하는 걸 추천합니다.

// Spring Security 를 사용한다면 Filter 설정하는게 맘 편함

@Configuration
@EnableWebSecurity
public class SecurityConfig {
    
   @Bean // CORS 용 Filter 만들기
   public CorsFilter corsFilter() {
      UrlBasedCorsConfigurationSource source
              = new UrlBasedCorsConfigurationSource();
      CorsConfiguration config
              = new CorsConfiguration();
      
      config.addAllowedOrigin("http://localhost:3000"); // origin

      // CORS 요청에 포함될 수 잇는 Request Header 들 설정
      config.addAllowedHeader("*");
      config.addAllowedMethod("*");     // CORS 허용할 HTTP Method

      // cookie, credential 이 CORS 요청에 포함될 수 있는지 설정
      config.setAllowCredentials(true);
      
      // CORS 적용될 endpoint pattern
      source.registerCorsConfiguration("/**", config);
      return new CorsFilter(source);
   }

   @Bean
   public SecurityFilterChain filterChain(HttpSecurity http)
           throws Exception {
      
      // Spring security CorsFilter 에 등록
      http.addFilterAt(corsFilter(), CorsFilter.class);

      return http.build();
   }
}

위처럼 설정할 경우 Spring SecurityCORS 요청을 필터링 할 수 있게 됩니다.

때문에 Preflight 요청처럼 빈 Request body 요청을 주요 Filter 전에 거스를 수 있었고 더이상 CORS 가 발생하지 않을 수 있었습니다.


말로만 듣던 CORS 를 처음 겪어 보았고, 차근차근 알아가며 지식을 넓힐 수 잇는 좋은 기회였습니다.

특히 CORS 가 결국에 브라우저 담당이라는 사실을 알게 되었고, FrontProxy 에 대해서도 알게 되었습니다.

결국 문제의 원인은 CORS 를 제대로 모르고 있었기 때문 이었지만 한번 제대로 공부하는 좋은 경험이었다 생각합니다.


📝 Reference

profile
나 같은게... 취준?!

0개의 댓글