Java/Spring Cloud Gateway

[SCG]Spring Cloud Gateway로 API 게이트웨이 구축 3편| WebFlux, WebClient 예제 | Spring Cloud Gateway 예제

a-몬드 2023. 12. 8. 21:00
반응형

이번에는 Spring Cloud Gateway로 구축한 api Gateway로 Filter 내에서 WebFlux로 다른 서버와 통신합니다. 

gateway 내에서 인증을 확인합니다. 

인증 시 유효하지 않을 경우 Error발생까지 하는 예제 한번 만들어 보겠습니다.

 

이번 예제를 통해 볼 수 있는 것 

1. Gateway Filter 내에서 다른 서버와 통신 

2. Gatewya Filter내에서 Error 발생 

 

WebClient로 다른 서버와 통신을 해보도록 하겠습니다. 

 

dependencies 주입 

webflux로 통신해야하니 webflux 의존성 주입해야 합니다. 

webflux를 통해 서버를 돌리면 spring web 의존성이 있어야 서버가 제대로 돌아갑니다. 

Json을 바디에 담아 요청을 보내야하기 때문에 json 의존성 주입할 것도 하나 추가했습니다.

implementation group: 'org.springframework.boot', name: 'spring-boot-starter-web', version: '2.6.1'
compileOnly group: 'com.googlecode.json-simple', name: 'json-simple', version: '1.1.1'
compileOnly 'org.springframework.boot:spring-boot-starter-webflux'

 

의존성만 주입하고 서버를 실행시키려 하면 아래와 같은 문구가 뜨면서 실행이 안될 것입니다. 

***************************
APPLICATION FAILED TO START
***************************

Description:

Spring MVC found on classpath, which is incompatible with Spring Cloud Gateway.

Action:

Please set spring.main.web-application-type=reactive or remove spring-boot-starter-web dependency.

Disconnected from the target VM, address: '127.0.0.1:52451', transport: 'socket'

Process finished with exit code 1

 

application.yml 수정 

spring:
  main:
    web-application-type: reactive

 

짠! 이러면 서버가 제대로 돌아갑니다. 

이제부터 진짜 Webclient로 다른 서버와 통신을 해보겠습니다. 

Webflux는 비동기가 가능해 성능이 좋고, 많이들 사용하지만 

저는 바로 응답을 받고 유효한지 아닌지 확인이 필요해  

동기로만 사용했습니다. 

 

서비스 서버 Auth Controller 작성 

진짜 auth는 아니지만 테스트용 auth라고 치자

@PostMapping("/auth")
@ResponseBody
public Object auth(@ModelAttribute TestApi authData){
        JSONObject result = new JSONObject();
        if (authData.getId().equals("test") && authData.getPassword().equals("1111")){
                result.put("access" , true );
                return result;
        }
        result.put("access" , false );
        return result;
}

 

CustomFilter 작성 

header에 id와 password값을 받아온다. 

 

import org.json.simple.JSONObject;
import org.springframework.cloud.gateway.filter.GatewayFilter;
import org.springframework.cloud.gateway.filter.factory.AbstractGatewayFilterFactory;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.http.server.reactive.ServerHttpResponse;
import org.springframework.stereotype.Component;
import lombok.Data;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;
import org.springframework.web.reactive.function.BodyInserters;
import org.springframework.web.reactive.function.client.WebClient;
import reactor.core.publisher.Mono;


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

    @Override
    public GatewayFilter apply(CustomFilter.Config config) {
        // 서비스 서버에 요청 보내기 전  Filter 작업
        return ((exchange, chain) -> {
            ServerHttpRequest request = exchange.getRequest();
            ServerHttpResponse response = exchange.getResponse();

            MultiValueMap<String, String> bodyMap = new LinkedMultiValueMap<>();
            bodyMap.add("Id", request.getHeaders().get("id").get(0));
            bodyMap.add("password", request.getHeaders().get("password").get(0));

            WebClient wc = WebClient.create("http://localhost:8888"); // webclient 생성
            ResponseEntity<JSONObject> res = wc.post()
                    .uri("/auth")
                    .contentType(MediaType.APPLICATION_FORM_URLENCODED)
                    .body(BodyInserters.fromFormData(bodyMap)) //    바디에 담아주기
                    .retrieve()
                    .toEntity(JSONObject.class)
                    .block();
            System.out.println("break point");
            // 서비스 서버에서 응답이 오고 난 후 Filter 작업
            return chain.filter(exchange).then(Mono.fromRunnable(() -> {

            }));

        });
    }

    @Data
    public static class Config{
        // Put the configuration properties...
        private String baseMessage;
        private boolean preLogger;
        private boolean postLogger;
    }
}

break point를 잡아서 res를 확인해보면 

access true 값을 응답 받는다. 

이제는 access 값에 따라서 false면 Error 발생시켜 client 쪽에 응답을 보내고  

true면 기존 서비스로 갈 수있게 처리해 줘야 한다. 

 

CustomFilter auth 서버 응답값 처리 

 

onError는 따로 빼서 작성했다. 

 private Mono<Void> onError(ServerWebExchange exchange, String err, HttpStatus httpStatus) {
        ServerHttpResponse response = exchange.getResponse();
        response.setStatusCode(httpStatus);

        JSONObject jsonObject = new JSONObject();
        jsonObject.put("code", httpStatus); // code도 담아주기
        jsonObject.put("msg", err); // 메세지 object에 담기

        // object -> String 변환 -> byte변환
        byte[] bytes = jsonObject.toString().getBytes(StandardCharsets.UTF_8);
        DataBuffer buffer = exchange.getResponse().bufferFactory().wrap(bytes);
        // byte로 변환한 errorObject buffer에 담아주기
        return response.writeWith(Mono.just(buffer));
    }

 

 

 

access false 테스트 

access true 테스트 

 

나의 일주일 간의 삽질이 3편으로 작성했다. 

혹시 저와 비슷한 케이스로 gateway 구축하셔야하는 분들은 제거 참고하세요 

안 되는 부분이 있다면 댓글로 질문을 주시면 제가 아는 한에서 답변드리겠습니다. 

제 글을 보시는 분들이 저와 같은 삽질을 안 했으면 좋겠네요ㅎㅎ

  

출처 : 

https://yunanp.tistory.com/67

https://docs.spring.io/spring-framework/reference/web/webflux-webclient.html

반응형