[Spring Cloud] Spring Cloud Gateway - 커스텀 필터 만들기

mrcocoball·2024년 1월 3일
0

Spring Cloud

목록 보기
2/8
post-custom-banner

해당 포스트는 Spring Cloud에 속한 기술들에 대한 개념과 주요 기술에 대해 알아보고 실무에 적용했었던 내용을 정리하는 포스트입니다.

1. 개요

Spring Cloud Gateway에서 기본적으로 제공하는 필터 외에도 사용자가 직접 커스텀 필터를 만들 수 있습니다. 커스텀 필터를 만드는 방법은 크게 두 가지가 있으며 본 포스팅에서는 두 가지 방법을 통해 커스텀 필터를 만들어보는 예제를 보여드리고자 합니다.

커스텀 필터를 만드는 방법 두 가지는 다음과 같습니다.

  • AbstractGatewayFilterFactory를 사용해서 GatewayFilter를 세팅
  • GlobalFilter 인터페이스를 구현한 GlobalFilter를 세팅

2. 커스텀 필터 - AbstractGatewayFilterFactory 사용

개요

추상 클래스인 AbstractGatewayFilterFactory를 내부 정적 Config 클래스와 함께 상속함으로서 커스텀 필터를 구현하는 방법으로, 요청을 전달하기 전에 수행되는 Pre 필터와 응답을 전달하기 전에 수행되는 Post 필터로 나뉘어지며 개발자 가이드 문서에는 다음과 같은 예시가 나와 있습니다.

// Pre 필터 예시
@Component
public class PreGatewayFilter extends AbstractGatewayFilterFactory<PreGatewayFilter.Config> {

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

	// apply를 재정의해야 하며 필터의 로직을 정의
    // chain.filter(exchange)를 호출하여 다음 필터로 체인을 전달하기 전에 필요한 작업을 수행
	@Override
	public GatewayFilter apply(Config config) {
		
        // Config 클래스에서 설정을 가져올 수 있음
        
		return (exchange, chain) -> {
        
            // chain.filter()를 리턴하기 전에 요청 조작과 관련된 코드를 추가
            
            ServerHttpRequest.Builder builder = exchange.getRequest().mutate();
            
            // 빌더를 사용하여 요청을 조작한 채로 다음 필터 체인으로 넘김
            return chain.filter(exchange.mutate().request(request).build());
		};
	}

	@Getter
    @Setter
	public static class Config {
        // 필터와 관련된 설정값을 추가할 수 있음
	}

}

// Post 필터 예시
@Component
public class PostGatewayFilter extends AbstractGatewayFilterFactory<PostGatewayFilter.Config> {

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

	// apply를 재정의해야 하며 필터의 로직을 정의
    // chain.filter(exchange).then(Mono...)를 호출하여 다음 필터로 체인을 전달하기 전에 필요한 작업을 수행
    // then(Mono)는 비동기 작업을 정의하며, Spring WebFlux에서 사용되는 리액티브 프로그래밍 패턴의 일종
	@Override
	public GatewayFilter apply(Config config) {
    
        // Config 클래스에서 설정을 가져올 수 있음
        
		return (exchange, chain) -> {
			return chain.filter(exchange).then(Mono.fromRunnable(() -> {
				ServerHttpResponse response = exchange.getResponse();
				
                // 응답 조작과 관련된 코드를 추가
			}));
		};
	}

	@Getter
    @Setter
	public static class Config {
        // 필터와 관련된 설정값을 추가할 수 있음
	}

}

위의 필터들을 컴포넌트로 등록한 후 어플리케이션 코드나 application.yml에서 사용할 수 있습니다.

spring:
  cloud:
    gateway:
      # 먼저 선언한 순서대로 필터가 적용됨 route 1 -> route 2
      routes:
      	# route 1, route의 id는 sample-internal
        - id: sample-internal
          uri: http://localhost:8082
          predicates:
            - Path=/sample/api/v1/internal/** # /sample/api/v1/internal/** 로 들어오는 요청에 반응
          filters:
            - PreGatewayFilter # filter 로직 적용 (커스텀 필터)

보통 이러한 커스텀 필터들은 특정 API 요청 / 응답에 대한 로깅을 하거나, API 게이트웨이가 인증 서버 역할을 하거나 1차 인증 검증을 한다던지, 그 외 접근해서는 안 되는 API에 접근을 하지 못하도록 하는 등의 다양한 기능을 추가할 때 활용됩니다.

예시 - API 요청 / 응답 로깅 필터

API 요청이 올 때 요청 정보를 로깅하고, 이에 대한 응답을 할 때의 응답 정보를 로깅하는 Pre / Post 필터를 만들 수 있습니다.

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

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

    @Override
    public GatewayFilter apply(Config config) {
        return (exchange, chain) -> {
            ServerHttpRequest request = exchange.getRequest();
            log.info("request information : {}, {}", request.getHeaders(), request.getURI());

            // reactive의 ServerHttpRequest
            ServerHttpRequest.Builder builder = exchange.getRequest().mutate();

            return chain.filter(exchange.mutate().request(builder.build()).build());
        };
    }

    @Getter
    @Setter
    public static class Config {
    }

}

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

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

    @Override
    public GatewayFilter apply(Config config) {
        return (exchange, chain) -> {
            return chain.filter(exchange).then(Mono.fromRunnable( () -> {
                ServerHttpResponse response = exchange.getResponse();
                log.info("response status : {} ", response.getStatusCode());
            }));
        };
    }

    @Getter
    @Setter
    public static class Config {
    }

}

예시 - 인증 헤더 검증 필터

API 요청에 Authorization 헤더가 존재하는지 검증하고 존재한다면 헤더에 있는 토큰을 검증하는 Pre 필터를 만들 수 있습니다.

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

    @Value("${spring.jwt.secret}")
    private String secret;

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

    @Override
    public GatewayFilter apply(AuthorizationHeaderFilter.Config config) {
        return (exchange, chain) -> {
            ServerHttpRequest request = exchange.getRequest();

            // 헤더에 Authorization이 없을 경우 (토큰을 발급받지 않은 경우)
            if (!request.getHeaders().containsKey(HttpHeaders.AUTHORIZATION))
                return onError(exchange, "No Authorization Header", HttpStatus.UNAUTHORIZED);

            String authorizationHeader = request.getHeaders().get(HttpHeaders.AUTHORIZATION).get(0);
            String token = authorizationHeader.replace("Bearer ", "");

            // 토큰 검증 실패 시
            if (!isTokenValid(token)) return onError(exchange, "Token is not valid", HttpStatus.UNAUTHORIZED);

            return chain.filter(exchange);
        };
    }

    // 예외 발생을 위한 메서드
    private Mono<Void> onError(ServerWebExchange exchange, String error, HttpStatus httpStatus) {
        ServerHttpResponse response = exchange.getResponse();
        
        response.setStatusCode(httpStatus);
        
        log.warn("error is occurred : {}", error);
        
        return Mono.error(UnAuthorizedException::new);
    }

    // 토큰 검증을 위한 메서드
    private boolean isTokenValid(String token) {
        String subject = null;
        
        try {
            subject = Jwts.parser()
                    .setSigningKey(secret.getBytes())
                    .parseClaimsJws(token)
                    .getBody()
                    .getSubject();
        } catch (Exception e) {
            log.warn("exception is occurred : {}", e.getMessage());
        }
        
        return !Strings.isBlank(subject);
    }

    @Getter
    @Setter
    public static class Config {}

}

3. 커스텀 필터 - GlobalFilter 구현

개요

앞에서 소개한 방법과 달리 GlobalFilter 인터페이스를 구현할 경우 글로벌 필터로 등록되어 전역적으로 동작합니다.
이 때, Ordered 인터페이스를 같이 구현할 경우 작동 우선순위를 지정해줄 수 있으며, 순서를 지정하지 않을 경우 글로벌 필터를 명시적으로 등록하지 않았거나 다른 필터보다 뒤에 코드가 작성되었을 경우 다른 Pre, Post 필터가 먼저 작동된 후에 작동되는 것으로 보입니다.

공식 문서에서는 다음과 같이 GlobalFilter, Ordered 인터페이스를 구현한 예제를 보여주고 있습니다.

@Bean
public GlobalFilter customFilter() {
    return new CustomGlobalFilter();
}

public class CustomGlobalFilter implements GlobalFilter, Ordered {

	// Pre 필터
    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
    	// chain.filter()를 리턴하기 전에 요청 조작과 관련된 코드를 추가
        log.info("custom global filter");
        return chain.filter(exchange);
    }

    @Override
    public int getOrder() {
        return -1;
    }
}

예시 - 내부 마이크로서비스로 요청 전달 시 헤더를 추가하는 글로벌 필터

Spring Cloud Gateway가 기본적으로 제공하는 헤더 추가 필터가 있지만 내부 통신 간 보안 강화를 위해 별도의 비즈니스 로직으로 특수한 헤더에 값을 추가하는 글로벌 필터를 만들 수 있습니다.

// GlobalGatewayTokenHeaderFilter
@Component
public class GlobalGatewayTokenHeaderFilter implements GlobalFilter, Ordered {

    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
        exchange.getRequest().mutate().header("X-Gateway-Header", generateGatewayToken());
        return chain.filter(exchange);
    }

    private String generateGatewayToken() {
        // 토큰 생성하는 비즈니스 로직
        return token;
    }

    @Override
    public int getOrder() {
        return -1;
    }
}


// 마이크로서비스 내 컨트롤러
@RestController
public class SampleServiceController {

    // 마이크로서비스 측 핸들러 메서드
    @GetMapping("/api/v1/sample")
    public String sample(@RequestHeader("X-Gateway-Header") String token) {
    	// 토큰 검증 메서드
        ...
    }

}

여담 - 개인적인 사용 사례

저같은 경우에는 로깅 필터와 인증 헤더 검증 필터, 게이트웨이 -> 마이크로서비스 요청 전달 시 헤더 추가 필터 외에도 일부 API의 접근을 막는 필터 등을 구현하였습니다.

그 외 암호화/복호화 필터를 개발하여 사용하는 사례도 많은 것 같고 다양한 커스텀 필터 구현 사례가 존재하니 실무에서 필요하신 내용이 있다면 한번 검색해보시는 것도 좋으리라 생각됩니다.

Appendix. 출처

https://cloud.spring.io/spring-cloud-gateway/reference/html/
https://cloud.spring.io/spring-cloud-gateway/multi/multi__developer_guide.html

profile
Backend Developer
post-custom-banner

0개의 댓글