MSA

[MSA] Part 6. Gateway Filter

cwchoiit 2023. 10. 10. 15:20
728x90
반응형
SMALL
728x90
반응형
SMALL

외부의 요청이 Gateway를 통해 들어올 때 요청을 가로채서 추가적인 작업을 수행할수도 있다. 이 때 사용되는 방법이 Filter이다.

 

 

Filter 설정하기

config 파일 하나를 만들기 위해 config라는 패키지 하나를 만들고 그 안에 FilterConfig.java 파일을 만들었다.

package com.example.tistorygateway.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 builder) {
        return builder.routes()
                .route(r -> r.path("/user-service/**")
                        .filters(f -> f.addRequestHeader("user-request", "user-request-header")
                                        .addResponseHeader("user-response", "user-response-header"))
                        .uri("http://localhost:8081"))
                .build();
    }
}

@Configuration 어노테이션을 추가하면 스프링 부트가 자동으로 이 클래스를 설정 처리해준다. 

그리고 그 클래스 내부에 @Bean으로 등록한 메소드를 하나 만들고 RouteLocatorBuilder 인스턴스를 build()한다.

이 RouteLocatorBuilder로 라우트 별 설정을 할 수 있다. 이 gateway로 들어오는 요청의 path가 /user-service로 시작하는 모든 요청에 대해 RequestHeader와 ResponseHeader를 추가한다. Header를 추가할 때 key/value쌍으로 추가하면 되는데 이렇게 추가를 할 수 있고 그 요청에 대한 URL을 http://localhost:8081로 보낸다는 의미에 uri()가 있다. 

 

이렇게 Config파일을 하나 만들고 서버를 재실행한 후 UserService에 새로운 Request Controller를 만들어보자.

 

 

UserService

package com.example.tistoryuserservice.controller;

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;

@Slf4j
@RestController
@RequestMapping("/user-service")
public class StatusController {

    @GetMapping("/welcome")
    public String welcomePage() {
        return "Welcome ! This is User Service.";
    }
	
    // 새롭게 추가한 부분
    @GetMapping("/message")
    public String message(@RequestHeader("user-request") String header) {
        log.info("header: {}", header);
        return "header check";
    }
}

새로운 GetMapping 메소드를 추가했고 Parameter로 RequestHeader를 받는 header 하나를 넣었다. 이렇게 파라미터에 요청 헤더를 받아올 수 있는데 이 헤더값을 로그로 찍은 후 "header check" 이라는 문자열을 응답 메세지로 반환한다.

 

이제 /user-service/message로 요청을 해서 filter가 동작하는지 확인해보자. 

 

 

 

Filter 동작 확인하기

위 사진처럼 /user-service/message로 요청을 보냈을 때 Response Headers에 User-Response라는 key가 담겨있는것을 확인할 수 있다. key에 대한 value는 'user-response-header'라고 명시되어 있음을 확인할 수 있다. 

 

여기서 한가지 더 확인할 수 있는건 Request Header에는 추가한 "user-request"가 들어가 있지 않는것을 볼 수 있는데 이는 filter를 거치기 전 request header에 정보이기 때문이다. 완벽하게 filter가 정상적으로 동작하고 있는것이다. 

그 filter를 거친 request header의 값은 어디서 확인하냐면 UserService의 Controller에서 파라미터에 넣었던 @RequestHeader를 통해 확인할 수 있다.

그러니까 흐름은 외부 요청 -> Gateway -> Filter -> UserService 이렇게 진행되고 위 브라우저에서는 외부 요청단계에 머물러 있는것이고 로그로 찍힌 상태에서는 UserService에 도달한 상태. 이렇게 외부 요청을 중간에 가로채서 추가적인 작업을 Filter를 통해서 수행할 수 있다.

 

 

이렇게 Filter를 설정할 수도 있고 application.yml 파일로도 Filter를 추가할 수 있다. 그것도 한 번 해 볼 생각이다.

 

 

application.yml 파일로 filter 추가하기

우선 위에서 등록해봤던 @Configuration을 주석처리하자. filter를 application.yml 파일로 설정할 생각이니까.

package com.example.tistorygateway.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 builder) {
        return builder.routes()
                .route(r -> r.path("/user-service/**")
                        .filters(f -> f.addRequestHeader("user-request", "user-request-header")
                                        .addResponseHeader("user-response", "user-response-header"))
                        .uri("http://localhost:8081"))
                .build();
    }
}

 

주석 처리했으면 이제 application.yml 파일에 다음과 같이 filter를 추가한다.

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

spring:
  application:
    name: api-gateway-service
  cloud:
    gateway:
      routes:
        - id: user-service
          uri: http://localhost:8081/user-service/**
          predicates:
            - Path=/user-service/**
          # application.yml 파일에 filter 추가
          filters:
            - AddRequestHeader=user-request, user-request-header2
            - AddResponseHeader=user-response, user-response-header2

추가한 필터가 하는 동작은 위에서 했던 내용과 정확히 일치한다.

Request Header에 key/value를 (user-request, user-request-header2).

Response Header에 key/value를 (user-response, user-response-header2). 

물론, value값만 차이를 구분하기 위해 뒤에 숫자 '2'를 추가했다. 

 

이제 application.yml 파일로 추가한 filter가 정상 동작하는지 또 한번 확인해보자.

 

 

application.yml에 추가한 filter 확인하기

확인해보면 역시 정상적으로 filter가 동작하고 있음을 확인할 수 있고 RequestHeader도 확인해보기 위해 로그를 확인한다.

 

 

 

CustomFilter 추가하기

사용자 정의 필터도 만들 수 있다. 이런 필터로는 사용자 인증 같은 인증된 유저만 접근가능하게 하는 경우가 제일 많이 사용되는데 이런 필터는 어떻게 만드는지 직접 구현해 볼것이다.

 

우선 filter라는 패키지를 하나 추가하고 CustomFilter.java 파일을 만든다.

package com.example.tistorygateway.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;

@Slf4j
@Component
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("[CustomFilter] Start: request id {}", request.getId());

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

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

@Component 어노테이션을 사용해서 이 클래스를 스프링에 빈으로 등록하고, AbstractGatewayFilterFactory를 상속받는다.

apply() 함수 안에 실제 필터로 무언가 할 내용을 입력하면 된다. 위 예시에서는 필터가 시작하는 시점과 끝나는 시점의 로그를 출력해 필터의 적용 및 시작과 끝을 좀 더 보기 수월하게 했다.

 

이렇게 설정을 했으면 이 필터를 적용해야한다. 커스텀 필터는 글로벌 필터가 아니기 때문에 필요한 서비스마다 필터 설정을 걸어주면 된다.

application.yml 파일에서 필터를 설정해주자.

 

 

Custom Filter 적용하기

application.yml 파일에서 방금 만든 CustomFilter를 추가한다.

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

spring:
  application:
    name: api-gateway-service
  cloud:
    gateway:
      routes:
        - id: user-service
          uri: http://localhost:8081/user-service/**
          predicates:
            - Path=/user-service/**
          filters:
#            - AddRequestHeader=user-request, user-request-header2
#            - AddResponseHeader=user-response, user-response-header2
            - CustomFilter

spring.cloud.gateway.routes.filters에 CustomFilter를 추가했고 그 전에 입력한 AddRequestHeader와 AddResponseHeader는 주석처리했다. 이렇게 설정한 후 재실행 시켜서 다시 UserService에 요청을 보내보자.

 

UserService가 응답할 수 있는 어떤 요청도 상관없이 요청을 보내보면 gateway service에서 확인할 수 있는 로그가 있다.

필터가 적용된 UserService에 대한 요청이 들어왔을 때 찍힌 로그가 보인다. 이렇듯 사용자 정의 필터를 원하는 서비스마다 적용시킬 수 있다.

 

 

 

GlobalFilter 추가하기

GlobalFilter는 gateway service로부터 들어오는 모든 요청에 대해 필터를 적용하는 것이다.

이 또한 CustomFilter를 만드는것과 비슷하게 만들 수 있다.

 

filter 패키지 안에 GlobalFilter.java 파일을 만들고 다음과 같이 작성했다.

package com.example.tistorygateway.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;

@Slf4j
@Component
public class GlobalFilter extends AbstractGatewayFilterFactory<GlobalFilter.Config> {

    public GlobalFilter() {
        super(Config.class);
    }

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

            if (config.isPreLogger()) {
                log.info("[GlobalFilter] Start: request id {}", request.getId());
            }

            return chain.filter(exchange).then(Mono.fromRunnable(() -> {
                if (config.isPostLogger()) {
                    log.info("[GlobalFilter] End: response code: {}", response.getStatusCode());
                }
            }));
        };
    }

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

GlobalFilter는 CustomFilter 만들 때와 거의 유사하다. 똑같다고 봐도 되는데 코드에서 달라지는 부분은 Config 클래스에 properties가 추가됐다. baseMessage, preLogger, postLogger 세 개의 필드를 추가했고 이 값은 application.yml 파일에서 지정해 줄 것이다.

 

 

GlobalFilter 적용하기

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

spring:
  application:
    name: api-gateway-service
  cloud:
    gateway:
      default-filters:
        - name: GlobalFilter
          args:
            baseMessage: Spring Global Filter
            preLogger: true
            postLogger: true
      routes:
        - id: user-service
          uri: http://localhost:8081/user-service/**
          predicates:
            - Path=/user-service/**
          filters:
#            - AddRequestHeader=user-request, user-request-header2
#            - AddResponseHeader=user-response, user-response-header2
            - CustomFilter

application.yml 파일에서 spring.cloud.gateway.default-filters에 GlobalFilter가 추가됐음을 확인할 수 있다. 이렇게 default-filters로 추가하면 어떤 라우트가 됐던간 이 gateway를 통과하는 모든 요청은 저 필터를 거친다. 

그리고, args로 Config 클래스에서 만든 세 가지 필드 baseMessage, preLogger, postLogger 값을 설정했다. 

 

이렇게 작성하고 gateway-service를 재실행해서 UserService에 요청을 날려보자. 그럼 gateway service에서 이런 로그를 확인할 수 있다. 

보면 GlobalFilter가 가장 먼저 시작하고 GlobalFilter가 끝나기 전 CustomFilter가 동작해서 끝나고 난 후 GlobalFilter가 마지막으로 끝난다. 모든 필터는 이렇게 동작한다. GlobalFilter로 설정한 필터가 제일 먼저 시작해서 제일 나중에 끝난다.

 

 

 

LoggingFilter 추가하기

필터를 하나 더 추가해서 적용했을 때 필터의 우선순위에 따라 필터가 적용되는 순서가 달라짐을 확인해보고 적절하게 사용할 수 있도록 해보자. CustomFilter, GlobalFilter를 만든 패키지에 LoggingFilter.java 파일을 만든다.

package com.example.tistorygateway.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;

@Slf4j
@Component
public class LoggingFilter extends AbstractGatewayFilterFactory<LoggingFilter.Config> {

    public LoggingFilter() {
        super(Config.class);
    }

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

            if (config.isPreLogger()) {
                log.info("[GlobalFilter] Start: request id {}", request.getId());
            }

            return chain.filter(exchange).then(Mono.fromRunnable(() -> {
                if (config.isPostLogger()) {
                    log.info("[GlobalFilter] End: response code: {}", response.getStatusCode());
                }
            }));
        };*/


        GatewayFilter filter = new OrderedGatewayFilter((exchange, chain) -> {
            ServerHttpRequest request = exchange.getRequest();
            ServerHttpResponse response = exchange.getResponse();

            log.info("[LoggingFilter]: baseMessage: {}", config.getBaseMessage());
            if (config.isPreLogger()) {
                log.info("[LoggingFilter]: Start request id {}", request.getId());
            }

            return chain.filter(exchange).then(Mono.fromRunnable(() -> {
                if (config.isPostLogger()) {
                    log.info("[LoggingFilter]: End response code {}", response.getStatusCode());
                }
            }));
        }, Ordered.LOWEST_PRECEDENCE);
        return filter;
    }

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

이번에는 Lambda식이 아니고 인스턴스 생성 후 인스턴스를 리턴하는 방식으로 구현해보자. 정확히 같은 내용인데 이렇게 된 코드를 Lambda 표현식으로도 사용할 수 있음을 이해하기 위해서 이렇게 작성했다.

 

다른건 다 똑같고 OrderedGatewayFilter()의 두번째 인자로 Ordered.LOWEST_PRECEDENCE를 적용하면 이 LoggingFilter가 가장 나중에 실행된다. Ordered.HIGHEST_PRECEDENCE도 있는데 이는 GlobalFilter보다도 더 먼저 실행된다. 그래서 그 차이를 확인해보자.

 

이 필터를 만들었으면 application.yml 파일에 적용해야한다. 

 

 

 

LoggingFilter 적용하기

 

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

spring:
  application:
    name: api-gateway-service
  cloud:
    gateway:
      default-filters:
        - name: GlobalFilter
          args:
            baseMessage: Spring Global Filter
            preLogger: true
            postLogger: true
      routes:
        - id: user-service
          uri: http://localhost:8081/user-service/**
          predicates:
            - Path=/user-service/**
          filters:
#            - AddRequestHeader=user-request, user-request-header2
#            - AddResponseHeader=user-response, user-response-header2
            - name: CustomFilter
            - name: LoggingFilter
              args:
                baseMessage: Logging Filter
                preLogger: true
                postLogger: true

어떠한 arguments도 가지지 않는 필터라면 그냥 필터 이름만 작성하면 되는데 그게 아니라면 name, args 키를 작성해줘야한다. 

이렇게 작성한 후 실행해서 gateway로 요청을 보내면 다음과 같은 로그가 찍힌다.

gateway에 요청을 보내고 로그를 확인해보면 다음과 같다.

GlobalFilter -> CustomFilter -> LoggingFilter 순으로 실행되고 이 역순으로 필터가 종료된다.

이렇게 필터에 우선순위를 최하위로 선언하면 가장 마지막에 실행이 되고 이번엔 최우선으로 선언해본다.

package com.example.tistorygateway.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;

@Slf4j
@Component
public class LoggingFilter extends AbstractGatewayFilterFactory<LoggingFilter.Config> {

    public LoggingFilter() {
        super(Config.class);
    }

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

            if (config.isPreLogger()) {
                log.info("[GlobalFilter] Start: request id {}", request.getId());
            }

            return chain.filter(exchange).then(Mono.fromRunnable(() -> {
                if (config.isPostLogger()) {
                    log.info("[GlobalFilter] End: response code: {}", response.getStatusCode());
                }
            }));
        };*/


        GatewayFilter filter = new OrderedGatewayFilter((exchange, chain) -> {
            ServerHttpRequest request = exchange.getRequest();
            ServerHttpResponse response = exchange.getResponse();

            log.info("[LoggingFilter]: baseMessage: {}", config.getBaseMessage());
            if (config.isPreLogger()) {
                log.info("[LoggingFilter]: Start request id {}", request.getId());
            }

            return chain.filter(exchange).then(Mono.fromRunnable(() -> {
                if (config.isPostLogger()) {
                    log.info("[LoggingFilter]: End response code {}", response.getStatusCode());
                }
            }));
        }, Ordered.HIGHEST_PRECEDENCE);
        return filter;
    }

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

Ordered.HIGHEST_PRECEDENCE로 두번째 인자를 추가해서 다시 실행해보면 다음과 같은 로그가 찍힌다.

이번엔 LoggingFilter가 가장먼저 실행됐다. 이렇게 필터의 우선순위를 지정할 수도 있다.

 

728x90
반응형
LIST