새소식

trouble-shooting

MSA에서 CORS 문제 해결하기

  • -

MSA에서 CORS 문제 해결하기

프로젝트 수행 중 CORS 문제를 맞닥드리게 되었고, 이를 해결한 과정을 기록하려 한다.

문제 상황

문제 상황을 알아보기 전에 프로젝트의 구조부터 설명한다.

프로젝트는 크게는 클라이언트 애플리케이션과 서버 애플리케이션으로 나눌 수 있다.
클라이언트는 리액트를 사용했고 서버는 스프링 Cloud를 통해 마이크로 서비스 아키텍처로 나누어져있다.

다시 서버를 세분화 하면 모든 요청의 진입점이자 요청의 인증 정보를 확인하는 API Gateway 서비스, 유저 등록, 조회 등 유저 정보를 관리하는 유저 서비스, 유저 정보를 통해 인증 토큰, 소셜 로그인 등을 책임지는 인증 서비스, 각 비즈니스 로직이 들어있는 비즈니스 서비스1, 2 등이 있다.

  • API Gateway
  • Auth Service
  • User Service
  • Business-1 Service
  • Business-2 Service

이 중 Auth Service 애플리케이션은 소셜 로그인 등을 위해 스프링 시큐리티를 사용했다.
추가로 소셜 로그인 성공 이후, 유저 정보를 이용하여 AccessToken, RefreshToken을 발급하고, RefreshToken은 응답 헤더에 쿠키 형태로 반환한다.

이때, Auth Service에는 CORS 정책 등을 고려하여 CORS 설정을 추가했다.
리프레시 토큰 쿠키로 토큰을 재발급하는 작업은 Auth Service에서만 진행하기 때문이었다.
개발 단계에서는 아직 프론트 애플리케이션의 도메인을 몰랐고, 테스트를 위해 모든 Origin을 허용하도록 Access-Control-Allow-Origin: *와 같이 헤더를 추가했다.

프로젝트가 어느정도 진행 된 후, 프론트와의 테스트를 위해 AWS EC2 인스턴스를 생성하여 개발 서버를 구축하고 여기에 각 서비스를 배포한 후, 도메인을 적용하였다.

이후 프론트 애플리케이션도 파이베이스를 통해 배포를 진행하여 서버 애플리케이션과 테스트를 진행하였다.

배포된 애플리케이션에서 소셜 로그인을 수행하고, 발급받은 토큰을 검증하는 API가 있었다.
로그인에 성공, 토큰을 검증받으려고 API가 요청되는 순간, 다음과 같은 CORS 문제가 발생하였다.

Access to fetch at 'https://api.come-on.ml/auth/validate' from origin 'https://come-on-c4b81.web.app' 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. If an opaque response serves your needs, set the request's mode to 'no-cors' to fetch the resource with CORS disabled.

오류 로그를 확인해보면, Preflight Request에 대한 응답에 Access-Control-Allow-Origin 헤더가 존재하지 않는다는 말이었다.

여기서 Preflight Request란 본 요청 전에 전송되는 예비 요청으로, 요청하는 출처와 리소스를 제공하는 출처가 다를 경우, 몇가지 조건을 만족하지 않는 요청은 Preflight Request를 전송하게 된다.
브라우저는 이 예비 요청에 대한 응답을 통해 본 요청이 CORS 정책을 만족하는 요청인지 확인하고, 만족하지 않는다면 위와 같이 본 요청을 전송하지 않고 CORS 오류 로그를 출력한다.

해결 과정

요청에 대한 응답을 확인해보니 Access-Control-Allow-Origin 헤더가 빠져있었다.
그렇기 때문에 브라우저는 CORS 정책을 만족하지 않는다 판단하고 오류를 발생시킨 것이었다.

cors-response

분명 Auth Service에 분명 모든 Origin을 허용하도록 설정했었다. 무슨 이유에서 해당 헤더 값이 빠진채로 응답이 온 것일까 의문이 들어 Api Gateway 서비스와 Auth 서비스에 로그를 남기도록 하고 다시 요청을 보냈다.

그런데 로그를 확인해봤더니, 게이트웨이와 인증 서비스 모두 어떤 로그도 찍히지 않았다.
인증 부분을 담당하던 프론트 팀원과 함께 백엔드 개발중인 팀원, 그리고 나까지 모두 멘붕이 왔고, 자료를 좀 찾아보기로 했었다.
그러다 백엔드 팀원분이 자료를 찾아보시고 해답을 얻어오셨다.

API Gateway CORS 설정

팀원분의 말에 의하면, Spring Cloud Gateway를 사용할 경우, Preflight Request는 실제 라우팅 설정한 Micro Service 들로 요청이 넘어가지 않고, 빈 응답을 반환한다고 한다.

이에 대한 코드를 직접 확인해봤다. 아래는 Gateway의 AbstractHandlerMapping 클래스의 getHandler 메서드이다.

public abstract class AbstractHandlerMapping extends ApplicationObjectSupport
        implements HandlerMapping, Ordered, BeanNameAware {

  private static final WebHandler NO_OP_HANDLER = exchange -> Mono.empty();

  @Override
  public Mono<Object> getHandler(ServerWebExchange exchange) {
    return getHandlerInternal(exchange).map(handler -> {
      if (logger.isDebugEnabled()) {
        logger.debug(exchange.getLogPrefix() + "Mapped to " + handler);
      }
      ServerHttpRequest request = exchange.getRequest();
      if (hasCorsConfigurationSource(handler) || CorsUtils.isPreFlightRequest(request)) {
        CorsConfiguration config = (this.corsConfigurationSource != null ?
            this.corsConfigurationSource.getCorsConfiguration(exchange) : null);
        CorsConfiguration handlerConfig = getCorsConfiguration(handler, exchange);
        config = (config != null ? config.combine(handlerConfig) : handlerConfig);
        if (config != null) {
          config.validateAllowCredentials();
        }
        if (!this.corsProcessor.process(config, exchange) || CorsUtils.isPreFlightRequest(request)) {
          return NO_OP_HANDLER;
        }
      }
      return handler;
    });
  }

}

코드를 확인해보니 CorsUtils.isPreFlightRequest(request)가 참인 경우에는 NO_OP_HANDLER <- Mono.empty()로 빈 값을 리턴하고 있었다.
API Gateway에 어떤 로그도 찍히지 않았던 이유는 이 때문이었고, 빈 값을 바로 리턴하기 때문에 Auth Service 까지도 요청이 넘어가지 않았던 것이었다.

문제에 대한 해결책은, Api Gateway에 CORS 정책과 관련하여 설정을 추가하는 것이었다.
이를 위해 Spring Cloud Gateway CORS Settings 공식 문서를 참조하여 API Gateway 내에서 CORS 설정을 하는 방법을 찾았고, 우리 프로젝트에 이를 적용하였다.

spring:
  cloud:
    gateway:
      globalcors:
        corsConfigurations:
          '[/**]':
            allowedOrigins:
             - "https://come-on-c4b81.web.app"
            allow-credentials: true   
            allowedHeaders:
              - x-requested-with
              - authorization
              - content-type
              - credential
              - X-AUTH-TOKEN
              - X-CSRF-TOKEN
            allowedMethods:
              - POST
              - GET
              - PUT
              - PATCH
              - OPTIONS
              - DELETE

위와 같이 설정한 후, 기존에 발생하던 No Access-Control-Allow-Origin header 문제는 해결할 수 있었다.

이제는 다른 문제가 발생하였다.

CORS - Multiple Value 오류

No Access-Control-Allow-Origin header 문제를 해결하고 나니, 아래와 같은 다른 오류 로그를 확인할 수 있었다.

The 'Access-Control-Allow-Origin' header contains multiple values 'https://come-on-c4b81.web.app', 'https://come-on-c4b81.web.app', but only one is allowed. Origin 'https://come-on-c4b81.web.app' is therefore not allowed access.

로그를 확인해보면 Access-Control-Allow-Origin 헤더에 값이 여러개가 추가되었고, 하나만 허용되기 때문에 발생하는 것이라 한다.

프로젝트 구조와 초기 설정을 생각해보면 두 개가 추가된 것이 당연하다.
기존에는 API Gateway에는 CORS 설정이 없고 Auth Service에만 CORS 설정이 되어있었다.
문제를 해결하다 보니 Gateway에 CORS 설정을 추가하게 되어 현재는 Gateway와 Auth Service에 CORS 설정이 중복으로 되어있는 상태이다.

따라서 Auth Service에 CORS 설정을 지웠고, 문제를 해결할 수 있었다.

MSA에서 CORS 문제 해결하기

포스팅 주소를 복사했습니다

이 글이 도움이 되었다면 공감 부탁드립니다.