본문 바로가기
spring cloud

API Gateway Service - Spring Cloud Gateway - Filter추가하기

by 킹차니 2022. 3. 1.

이제 이전에 만든 API Gateway Service에 필터를 추가해보자.

필터는 Pre Filter(사전 필터)와 Post Filter(사후 필터)로 나뉘며, Property혹은 Java Code로 직접 로직을 작성할 수도 있다.

 

먼저 자바코드를 사용하여 필터를 추가하는 방법을 알아보자.

 

 

일단 이전에 apigateway-service에서 작성한 yml의 cloud관련 설정을 모두 주석처리한다.

server:
  port: 8000

eureka:
  client:
    register-with-eureka: false
    fetch-registry: false
    service-url:
      defaultZone: http://localhost:8761/eureka
spring:
  application:
    name: apigateway-service
#  cloud:
#    gateway:
#      routes:
#        - id: first-service
#          uri: http://localhost:8081
#          predicates:
#            - Path=/first-service/**
#        - id: second-service
#          uri: http://localhost:8082
#          predicates:
#            - Path=/second-service/**

 

그리고 아래와 같이 FilterConfig클래스를 작성한다.

package com.example.apigatewayservice.config;

import org.springframework.cloud.gateway.route.RouteLocator;
import org.springframework.cloud.gateway.route.builder.RouteLocatorBuilder;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class FilterConfig {

    @Bean
    public RouteLocator gatewayRoutes(RouteLocatorBuilder routeLocatorBuilder) {
        return routeLocatorBuilder
                .routes()
                    .route(r -> r.path("/first-service/**")// 1.path를 확인하고
                            .filters(f -> f// 2.필터를 적용하여
                                    .addRequestHeader("first-request", "first-service-requestHeader")
                                    .addResponseHeader("first-response", "first-service-responseHeader"))
                            .uri("http://localhost:8081"))//uri로 이동시켜준다.

                    .route(r -> r.path("/second-service/**")
                            .filters(f -> f
                                    .addRequestHeader("second-request", "second-service-requestHeader")
                                    .addResponseHeader("second-response", "second-service-responseHeader"))
                            .uri("http://localhost:8082"))
                    .build();
    }
}

RouteLocatorBuilder를 사용하여 라우팅에대한 설정과 필터를 추가할 수 있다. 위의 필터에서는 들어오는 요청과 나가는 응답에 header를 추가해주었다.

 

그리고 정말 위에서 적용한 필터를 거쳐 헤더에 정보가 추가되었는지 확인하기 위해 first-service와 second-service의 컨트롤러에 아래와 같은 메소드를 추가해주었다.

 

package com.example.firstservice;

import lombok.extern.slf4j.Slf4j;
import org.springframework.core.env.Environment;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestHeader;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import javax.servlet.http.HttpServletRequest;
import java.util.Collections;
import java.util.Enumeration;

@RestController
@RequestMapping("/first-service")
@Slf4j
public class FirstServiceController {

    @GetMapping("/welcome")
    public String welcome() {
        return "Welcome to the First service.";
    }

	// message 메서드 추가
    @GetMapping("/message")
    public String message(@RequestHeader("first-request") String header) {
        log.info(header);
        return "Hello World in First Service.";
    }
}
package com.example.secondservice;

import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestHeader;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequestMapping("/second-service")
@Slf4j
public class SecondServiceController {
    @GetMapping("/welcome")
    public String welcome() {
        return "Welcome to the Second service.";
    }
    // message 메서드 추가
    @GetMapping("/message")
    public String message(@RequestHeader("second-request") String header) {
        log.info(header);
        return "Hello World in Second Service.";
    }
}

이제 이를 테스트해보자.

first-service/message로 요청을 보내고 받은 응답의 headers을 보면 필터에서 추가한 헤더가 보인다.

 

그리고 아래와 같이 콘솔창에서도 추가된 header가 보인다.

 

 

 


이번에는 yml파일(Property)에 직접 필터를 추가해보자.

이전에 주석처리한 yml 코드를 다시 풀고 아래와 같이 filter를 추가한다.

server:
  port: 8000

eureka:
  client:
    register-with-eureka: false
    fetch-registry: false
    service-url:
      defaultZone: http://localhost:8761/eureka

spring:
  application:
    name: apigateway-service
  cloud:
    gateway:
      routes:
        - id: first-service
          uri: http://localhost:8081
          predicates:
            - Path=/first-service/**
          filters:
            - AddRequestHeader=first-request, first-request-header2 # 앞에가 key, 뒤에가 value
            - AddResponseHeader=first-response, first-response-header2
        - id: second-service
          uri: http://localhost:8082
          predicates:
            - Path=/second-service/**
          filters:
            - AddRequestHeader=second-request, second-request-header2
            - AddResponseHeader=second-response, second-response-header2


이제 이를 테스트해보면 역시 잘 작동되는 것을 알 수 있다.

 

 


Spring Cloud Gateway - Custom Filter

 

이제 apigateway-service에 직접 정의하는 간단한 custom filter를 적용해보자.

 

이를 위해서는 AbstractGatewayFilter<C>를 extends하고 apply메서드를 구현하면 된다.

package com.example.apigatewayservice.filter;

import lombok.extern.slf4j.Slf4j;
import org.springframework.cloud.gateway.filter.GatewayFilter;
import org.springframework.cloud.gateway.filter.factory.AbstractGatewayFilterFactory;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.http.server.reactive.ServerHttpResponse;
import org.springframework.stereotype.Component;
import reactor.core.publisher.Mono;

@Component
@Slf4j
public class CustomFilter extends AbstractGatewayFilterFactory<CustomFilter.Config> {
    public CustomFilter() {super(Config.class);}

    @Override
    public GatewayFilter apply(Config config) {
        //Custom Pre Filter
        return (exchange, chain) ->{
            ServerHttpRequest request = exchange.getRequest();
            ServerHttpResponse response = exchange.getResponse();

            log.info("Custom Pre filter: request id -> {}", request.getId());

            //Custom Post Filter
            return chain.filter(exchange).then(Mono.fromRunnable(()->{
                log.info("Custom Post filter: response code -> {}", response.getStatusCode());
            }));
        };
    }

    public static class Config{
        //Put the configuration properties
    }
}

해당 필터는 request의 id를 출력하고 response시에 statuscode를 출력한다.

 

이제 yml 파일에 해당 필터를 등록하자. 

 

그리고 이를 테스트하기 위해 first-service와 second-service에 아래의 메서드들을 각각 추가해주었다.

@GetMapping("/check")
public String check() {
    return "Hi, there. This is a message from First Service";
}
@GetMapping("/check")
public String check() {
    return "Hi, there. This is a message from Second Service";
}

 

이제 포스트맨으로 테스트해보자.

 

위와 같은 두번의 요청을 보낸 결과 아래와 같이 apigateway-service에 로그가 남는 것을 볼 수 있다.

 


Spring Filter Gateway - Global Filter

글로벌 필터는 공통적으로 모두에 적용되는 필터라는 점에서 커스텀 필터와 차이가 있다. 즉 이전의 Custom 필터를 적용한 yml파일을 보면

Custom필터를 적용하기 위해 각각의 서비스에 따로 등록을 해주어야 했다.

 

하지만 글로벌 필터는 이러한 과정없이 모든 서비스에 적용된다.

 

Custom필터를 만드는 것과 똑같이 만들어주면 된다.

package com.example.apigatewayservice.filter;

import lombok.Data;
import lombok.extern.slf4j.Slf4j;
import org.springframework.cloud.gateway.filter.GatewayFilter;
import org.springframework.cloud.gateway.filter.factory.AbstractGatewayFilterFactory;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.http.server.reactive.ServerHttpResponse;
import org.springframework.stereotype.Component;
import reactor.core.publisher.Mono;

@Component
@Slf4j
public class GlobalFilter extends AbstractGatewayFilterFactory<GlobalFilter.Config> {
    public GlobalFilter() {super(Config.class);}

    @Override
    public GatewayFilter apply(Config config) {
        //Custom Pre Filter
        return (exchange, chain) ->{
            ServerHttpRequest request = exchange.getRequest();
            ServerHttpResponse response = exchange.getResponse();
            log.info("Global Filter baseMessage: -> {}", config.getBaseMessage());

            if(config.isPreLogger()) log.info("Global Filter Start: request id -> {}", request.getId());
            //Custom Post Filter
            return chain.filter(exchange).then(Mono.fromRunnable(()->{
                if(config.isPostLogger()) log.info("Global Filter End: response status code -> {}", response.getStatusCode());
            }));
        };
    }

    @Data
    public static class Config{
        private String baseMessage;
        private boolean preLogger;
        private boolean postLogger;
    }
}

 

이제 해당 글로벌 필터를 yml에서 등록해준다.

name.args 이하에 config의 인자들을 넣어준 것을 알 수 있다.

이를 테스트해보자.

결과를 보면 글로벌 필터 -> 커스텀 필터 순으로 실행되는 것을 볼 수 있다.

 

 

 


이번에는 LoggingFilter를 만들어서 second-service에만 적용해보자.

 

LoggingFilter역시 CustomFilter와 만드는 방법은 같은데, 이번엔 GatewayFilter를 직접 생성하여 반환하는 방식으로 만들어보자.(아래 코드에서 OrderedGatewayFilter를 생성했는데 이는 GatewayFilter를 implements하고 있다.)

package com.example.apigatewayservice.filter;

import lombok.Data;
import lombok.extern.slf4j.Slf4j;
import org.springframework.cloud.gateway.filter.GatewayFilter;
import org.springframework.cloud.gateway.filter.OrderedGatewayFilter;
import org.springframework.cloud.gateway.filter.factory.AbstractGatewayFilterFactory;
import org.springframework.core.Ordered;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.http.server.reactive.ServerHttpResponse;
import org.springframework.stereotype.Component;
import reactor.core.publisher.Mono;

@Component
@Slf4j
public class LoggingFilter extends AbstractGatewayFilterFactory<LoggingFilter.Config> {
    public LoggingFilter() {super(Config.class);}

    @Override
    public GatewayFilter apply(Config config) {

        return new OrderedGatewayFilter((exchange, chain) -> {
            ServerHttpRequest request = exchange.getRequest();
            ServerHttpResponse response = exchange.getResponse();
            log.info("Logging Filter baseMessage: -> {}", config.getBaseMessage());

            if (config.isPreLogger()) log.info("Logging Filter Start: request id -> {}", request.getId());
            //Custom Post Filter
            return chain.filter(exchange).then(Mono.fromRunnable(() -> {
                if (config.isPostLogger())
                    log.info("Logging Filter End: response status code -> {}", response.getStatusCode());
            }));
        }, Ordered.LOWEST_PRECEDENCE);//제일 낮은 순위로 필터를 적용
    }

    @Data
    public static class Config{
        private String baseMessage;
        private boolean preLogger;
        private boolean postLogger;
    }
}

 

 

이제 이를 테스트해보자.

first-service 호출 시에는 글로벌 필터와 커스텀 필터만 적용되지만

 

 

second-service를 호출하면 로깅필터도 적용되는 것을 알 수 있다.

 

 

gateway, handler, filter들이 적용되는 순서들은 아래와 같다.

 

 

출처 : 인프런 Lee Dowon님의 강의와 PDF 자료
https://www.inflearn.com/course/%EC%8A%A4%ED%94%84%EB%A7%81-%ED%81%B4%EB%9D%BC%EC%9A%B0%EB%93%9C-%EB%A7%88%EC%9D%B4%ED%81%AC%EB%A1%9C%EC%84%9C%EB%B9%84%EC%8A%A4