해당 포스트는 Spring Cloud에 속한 기술들에 대한 개념과 주요 기술에 대해 알아보고 실무에 적용했었던 내용을 정리하는 포스트입니다.
Spring Cloud Gateway에서 기본적으로 제공하는 필터 외에도 사용자가 직접 커스텀 필터를 만들 수 있습니다. 커스텀 필터를 만드는 방법은 크게 두 가지가 있으며 본 포스팅에서는 두 가지 방법을 통해 커스텀 필터를 만들어보는 예제를 보여드리고자 합니다.
커스텀 필터를 만드는 방법 두 가지는 다음과 같습니다.
AbstractGatewayFilterFactory
를 사용해서 GatewayFilter를 세팅GlobalFilter
인터페이스를 구현한 GlobalFilter를 세팅추상 클래스인 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 요청이 올 때 요청 정보를 로깅하고, 이에 대한 응답을 할 때의 응답 정보를 로깅하는 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 {}
}
앞에서 소개한 방법과 달리 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의 접근을 막는 필터 등을 구현하였습니다.
그 외 암호화/복호화 필터를 개발하여 사용하는 사례도 많은 것 같고 다양한 커스텀 필터 구현 사례가 존재하니 실무에서 필요하신 내용이 있다면 한번 검색해보시는 것도 좋으리라 생각됩니다.
https://cloud.spring.io/spring-cloud-gateway/reference/html/
https://cloud.spring.io/spring-cloud-gateway/multi/multi__developer_guide.html