본문 바로가기
Spring

Spring Cloud Gateway CORS 주의사항

by ybs 2021. 1. 24.
반응형

CORS 요청을 받기 위해 서버가 해줘야 하는 작업들이 있다.

그런데 Spring Cloud Gateway 는 프록시 서버이기 때문에 조금 더 신경써야 하는 부분이 있어 정리했다.

※ CORS 기본 개념까지 같이 설명하기엔 글이 너무 길어져서 안다는 전제로 작성했다.

 

먼저 Spring Cloud Gateway 는 CORS 요청을 디폴트로 막는다.

CORS 허용하는 방법

1) CorsWebFilter

첫번째 방법은 CorsWebFilter 를 이용하는 것인데, 헷갈리지 말아야 될 게 있다.
CorsWebFilter 는 Spring Cloud Gateway 에서 제공하는 필터가 아니다. org.springframework.web.cors.reactive 에서 제공하는 필터다. 그래서 Spring Cloud Gateway 에 등록된 필터보다 먼저 동작한다.

 

빈 등록을 위해선 아래 코드처럼 CorsConfigurationSource 를 생성해 CorsWebFilter 에 넣어준다.

 

@Bean
public CorsConfigurationSource corsConfigurationSource() {
    UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
    CorsConfiguration corsConfiguration = new CorsConfiguration();
    corsConfiguration.addAllowedHeader("*");
    corsConfiguration.addAllowedOrigin("*");
    corsConfiguration.addAllowedMethod("*");
    corsConfiguration.setAllowCredentials(true);
    source.registerCorsConfiguration("/**", corsConfiguration);
    return source;
}

@Bean
public CorsWebFilter corsWebFilter() {
    return new CorsWebFilter(corsConfigurationSource());
}

 

그리고 CorsWebFilter 의 filter 로직을 보면 어떻게 동작하는지 알 수 있다.

@Override
public Mono<Void> filter(ServerWebExchange exchange, WebFilterChain chain) {
    ServerHttpRequest request = exchange.getRequest();
    CorsConfiguration corsConfiguration = this.configSource.getCorsConfiguration(exchange);
    boolean isValid = this.processor.process(corsConfiguration, exchange);
    if (!isValid || CorsUtils.isPreFlightRequest(request)) {
        return Mono.empty();
    }
    return chain.filter(exchange);
}

 

CORS 요청인지 확인하고 만약 valid 하지 않거나 preflight 요청이면 다음 필터로 보내지 않고 Mono.empty() 로 리턴해버린다.
this.processor.process(corsConfiguration, exchange) 메서드에서는 valid 체크 뿐 만 아니라 preflight 일 때 응답으로 보내줄 CORS 관련 헤더들을 셋팅하는 작업도 수행한다.

 

여기서 preflight 일 때 리턴해버리는게 주의할 점인데 자세한 내용은 뒤에서 설명하겠다.

2) GlobalCorsProperties

두번째 방법은 GlobalCorsProperties 를 이용하는 방법이다.
기본적으로 GatewayAutoConfiguration 에 GlobalCorsProperties 가 빈으로 등록되는데

 

@ConfigurationProperties("spring.cloud.gateway.globalcors")
public class GlobalCorsProperties {

    private final Map<String, CorsConfiguration> corsConfigurations = new LinkedHashMap<>();

    public Map<String, CorsConfiguration> getCorsConfigurations() {
        return corsConfigurations;
    }

}

 

spring.cloud.gateway.globalcors 설정이 프로퍼티 파일에 있으면 corsConfigurations 변수에 담기게 된다.

참고 : https://cloud.spring.io/spring-cloud-gateway/multi/multi__cors_configuration.html

 

10. CORS Configuration

The gateway can be configured to control CORS behavior. The "global" CORS configuration is a map of URL patterns to Spring Framework CorsConfiguration. application.yml.  spring: cloud: gateway: globalcors: corsConfigurations: '[/**]': allowedOrigins: "htt

cloud.spring.io

그리고 Spring Cloud Gateway 에서 주로 쓰이는 RoutePredicateHandlerMapping(AbstractHandlerMapping 상속받음) 에 GlobalCorsProperties 가 들어간다.

 

@Bean
public RoutePredicateHandlerMapping routePredicateHandlerMapping(
        FilteringWebHandler webHandler, RouteLocator routeLocator,
        GlobalCorsProperties globalCorsProperties, Environment environment) {
    return new RoutePredicateHandlerMapping(webHandler, routeLocator,
            globalCorsProperties, environment);
}

 

그리고 라우팅된 요청이 왔을 때 getHandler 코드를 보면(실제 구현체는 AbstractHandlerMapping에 있음) CORS 로직이 있다.

@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 (!this.corsProcessor.process(config, exchange) || CorsUtils.isPreFlightRequest(request)) {
            return REQUEST_HANDLED_HANDLER; // REQUEST_HANDLED_HANDLER 는 exchange -> Mono.empty() 다.
        }
    }
    return handler;
});
}

아까 CorsWebFilter 로직과 비슷하다. 여기서도 preflight 요청이면 리턴한다.

문제점

아까 코드에서 봤듯이 Spring Cloud Gateway 는 preflight 요청 일 때 뒷단 서버로 보내지 않고 리턴 시켜버린다. 다시말해 실제 뒷단 서버에서 preflight 요청을 받아 응답을 준게 아니다.

문제는 Spring Cloud Gateway 에서 준 preflight 응답에서 Access-Control-Allow-Origin 값이 있어야 하는데 정할 수가 없다. 뒷단 서버들의 도메인들이 다 다르기 때문이다. 설령 뒷단 서버 도메인들을 다 추가한다 하더라도 동적으로 새롭게 늘어갈 때마다 추가해줘야 한다. 그리고 allow origin 검사가 exact matching 이기 때문에 *.ybs.com 같이 패턴을 넣는것도 불가능하다.

그래서 Access-Control-Allow-Origin*로 해줄 수 밖에 없지만 이마저도 만족스럽지 않다.

본 요청에서 Access-Control-Allow-Origin 헤더를 또 보내서 두개가 되버려 Spring Cloud Gateway 에서 헤더 중복 제거를 해줘야 한다.

 

더 큰 문제는 Access-Control-Allow-Credentials 이다. Spring Cloud Gateway 에서 True/False 를 결정할 수 없다. 뒷단 서버에서 정해줘야 한다.

 

결국 이 문제를 해결하려면 CORS 요청을 Spring Cloud Gateway 에서 검사하지 않고 그냥 통과 시켜야 한다.

해결 방법

먼저 CorsWebFilter, GlobalCorsProperties 둘 다 사용하면 안된다.

그리고 RoutePredicateHandlerMapping 클래스의 getHandler 메서드에서(실제 구현체는 AbstractHandlerMapping에 있음) 강제로 CORS 체크를 하기 때문에 getHandler 메서드를 오버라이딩 해야 된다.

 

아래 코드는 RoutePredicateHandlerMapping 을 상속받은 PassCorsRoutePredicateHandlerMapping 을 새롭게 만들어 오버라이딩했다.

public class PassCorsRoutePredicateHandlerMapping extends RoutePredicateHandlerMapping {

    private static final Logger logger = LoggerFactory.getLogger(PassCorsRoutePredicateHandlerMapping.class);

    public PassCorsRoutePredicateHandlerMapping(FilteringWebHandler webHandler, RouteLocator routeLocator,
            GlobalCorsProperties globalCorsProperties, Environment environment) {
        super(webHandler, routeLocator, globalCorsProperties, environment);
    }

    @Override
    public Mono<Object> getHandler(ServerWebExchange exchange) {
        logger.info("[PassCorsRoutePredicateHandlerMapping] getHandler");
        return getHandlerInternal(exchange).map(handler -> {
            logger.info(exchange.getLogPrefix() + "Mapped to " + handler);

            // CORS 체크 로직 제거

            return handler;
        });
    }
}

 

변경사항은 CORS 로직만 제거 했다. 그리고 빈등록만 해주면 된다.

@Bean
@Primary
public RoutePredicateHandlerMapping passCorsRoutePredicateHandlerMapping(
        FilteringWebHandler webHandler, RouteLocator routeLocator,
        GlobalCorsProperties globalCorsProperties, Environment environment) {
    return new PassCorsRoutePredicateHandlerMapping(webHandler, routeLocator,
            globalCorsProperties, environment);
}

 

cf) 이 글을 작성하는 2021년 1월 기준으로, CORS preflight 요청을 그냥 통과시키는 옵션은 제공하지 않는다.
https://github.com/spring-cloud/spring-cloud-gateway/pull/1883에서 PR은 확인했지만 아직 머지되지 않았다.

 

반응형

'Spring' 카테고리의 다른 글

request body 한번만 읽어올수 있는 제약  (0) 2021.06.09
Spring Web MVC 구조 논의 1편  (0) 2021.06.06
useInsecureTrustManager 옵션  (0) 2021.01.20
request body memory leak  (0) 2021.01.20
개발 이슈 모음  (0) 2021.01.19