외부의 요청이 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가 가장먼저 실행됐다. 이렇게 필터의 우선순위를 지정할 수도 있다.
'MSA' 카테고리의 다른 글
[MSA] Part 8. H2 Database 연동 그리고 'ddl-auto' property (0) | 2023.10.11 |
---|---|
[MSA] Part 7. API Gateway를 Eureka에 등록하기 (0) | 2023.10.10 |
[MSA] Part 5. API Gateway (0) | 2023.10.06 |
[MSA] Part 4. Service 등록 (User) (0) | 2023.10.06 |
[MSA] Part 3. Spring Cloud Netflix Eureka (0) | 2023.10.06 |