[API Gateway + Refresh JWT 인증서버 구축하기] Spring boot + Spring Cloud Gateway + Redis + mysql JPA 2편

Sieun Sim·2020년 5월 31일
2

서버개발캠프

목록 보기
14/21

RouteLocator 작성하기

먼저 라우팅을 담당할 RouteLocator를 최상위의 Application.java 파일에 써준다.
RouteLocatorBuilder 는 predicates와 fileters들을 사용자의 routes에 추가해 request/response를 커스터마이징하는 등 조건에 따라 컨트롤할 수 있게 해준다.

package com.quadcore.gw2;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.gateway.route.RouteLocator;
import org.springframework.cloud.gateway.route.builder.RouteLocatorBuilder;
import org.springframework.context.annotation.Bean;

@SpringBootApplication
public class Gw2Application {

    public static void main(String[] args) {
        SpringApplication.run(Gw2Application.class, args);
    }

    @Bean
    public RouteLocator customRouteLocator(RouteLocatorBuilder builder) {

        return builder.routes()
                .route("path_route",  r-> r.path("/new")
                        .filters(f -> f.addRequestHeader("Hello", "World")
                        .rewritePath("/new", "/"))
                        .uri("http://localhost:8083/"))
                .build();
    }
}

현재 Gateway 프로젝트의 포트는 8080, 인증 서버의 포트는 8083이다. 기본적으로는 route.uri와 route.path를 다르게 취급해서 r.path("/new").uri("http://localhost:8083/") 만으로는 http://localhost:8083/8083으로 가버린다. 이를 위해 rewritePath(원래루트, 바꿀루트) 필터를 사용하는데 필터를 여러개 사용할 때는 filters(f-> f.필터1.필터2) 식으로 이어서 쓸 수 있다. 웬만하면 필터 이름으로 어떤 일을 하는 필터인지 알 수 있다.

$ curl -H "Test: ji" http://localhost:8080/new

커맨드로 요청을 보내면 헤더와 기타 속성들을 로그로 확인할 수 있다.

2020-01-28 15:28:56.922  INFO 25826 --- [or-http-epoll-6] com.quadcore.gw2.jwt.JwtRequestFilter    : Request:[Host:"localhost:8080", User-Agent:"curl/7.58.0", Accept:"*/*", Test:"ji", Hello:"World"]
2020-01-28 15:28:56.923  INFO 25826 --- [or-http-epoll-6] com.quadcore.gw2.jwt.JwtRequestFilter    : attributes: {org.springframework.cloud.gateway.support.ServerWebExchangeUtils.gatewayRequestUrl=http://localhost:8083/, org.springframework.cloud.gateway.support.ServerWebExchangeUtils.uriTemplateVariables={}, org.springframework.cloud.gateway.support.ServerWebExchangeUtils.routeWeight={}, org.springframework.cloud.gateway.support.ServerWebExchangeUtils.gatewayOriginalRequestUrl=[http://localhost:8080/new], org.springframework.cloud.gateway.support.ServerWebExchangeUtils.gatewayHandlerMapper=RoutePredicateHandlerMapping, org.springframework.web.server.ServerWebExchange.LOG_ID=4864b470, org.springframework.cloud.gateway.support.ServerWebExchangeUtils.gatewayRoute=Route{id='path_route', uri=http://localhost:8083/, order=0, predicate=Paths: [/new], match trailing slash: true, gatewayFilters=[[[AddRequestHeader Hello = 'World'], order = 0], [[RewritePath /new = '/'], order = 0]], metadata={}}}

인증 서버에서 http://localhost:8083/에 매핑되는 컨트롤러는 이렇다.

    @GetMapping(path="/")
    public String test() {
        return "TEST";
    }

Spring Cloud Gateway Filter 추가하기

Spring Cloud Gateway에서 Servlet이 아닌 Webflux 기반으로 작성하면 MVC에서 썼던 것과는 또 다른 모습을 볼 수 있다. 기존의 HttpServletRequest와 HttpServletResponse와 또 다르게 ServerWebExchange 라는 것을 사용한다.

  • ServerWebExchange

    Contract for an HTTP request-response interaction. Provides access to the HTTP request and response and also exposes additional server-side processing related properties and features such as request attributes.

Method Detail

getRequest

ServerHttpRequest getRequest()
Return the current HTTP request.

getResponse

ServerHttpResponse getResponse()
Return the current HTTP response.

getAttributes

Map<String,Object> getAttributes()
Return a mutable map of request attributes for the current exchange.

서블릿과는 다르지만 결국 ServerHttpRequest, ServerHttpResponse 라는 Http 요청/응답 객체를 얻을 수 있다. 사용법이 조금 다를 뿐 동일하게 헤더를 가져오는 등 사용할 수 있다. 헤더를 가져오려면 ServerWebExchange.getRequest().getHeaders() 로 가져올 수 있다.

다음과 같이 매 요청마다 filter를 이용해 ServerWebExchange를 가져다 분해해볼 수 있다. getHeaders를 통해 map으로 받은 헤더들에서 JWT를 잡아 여기서 파싱하고 검사할것이다.

필터도 종류가 여러개 있는데,
GlobalFilter 는 모든 요청마다 실행되는 필터다. 필터를 손수 추가해줄 필요 없이 인터페이스를 사용하는 것만으로 필터가 바로 적용된다.
AbstractGatewayFilterFactory 는 좀더 간단하고 심플하게 그야말로 추상적인 필터를 구현할 수 있게 해준다. builder를 이용해 직접 필터를 걸어줘야 한다.

GlobalFilter

import org.slf4j.Logger; import org.slf4j.LoggerFactory;
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.cloud.gateway.filter.GlobalFilter;
import org.springframework.stereotype.Component;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;

@Component  
public class JwtRequestFilter implements GlobalFilter {
  final Logger logger = LoggerFactory.getLogger(JwtRequestFilter.class);

  @Override
  public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
      try {
          String token = exchange.getRequest().getHeaders().get("Authorization").get(0).substring(7);
      } catch (NullPointerException e) {
          logger.warn("no token.");
          exchange.getResponse().setStatusCode(HttpStatus.valueOf(401));
          logger.info("status code :" + exchange.getResponse().getStatusCode());
          return chain.filter(exchange);
      }
      return chain.filter(exchange);
  }
}

AbstractGatewayFilterFactory

  • Filter
package com.quadcore.gw2.jwt;

import org.slf4j.Logger;  
import org.slf4j.LoggerFactory;  
import org.springframework.cloud.gateway.filter.GatewayFilter;  
import org.springframework.cloud.gateway.filter.factory.AbstractGatewayFilterFactory;  
import org.springframework.http.HttpStatus;  
import org.springframework.stereotype.Component;

@Component  
public class JwtRequestFilter extends  
AbstractGatewayFilterFactory<JwtRequestFilter.Config> {

  final Logger logger =
          LoggerFactory.getLogger(JwtRequestFilter.class);

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

      public Config(String baseMessage, boolean preLogger, boolean postLogger) {
          this.baseMessage = baseMessage;
          this.preLogger = preLogger;
          this.postLogger = postLogger;
      }
  }

  @Override
  public GatewayFilter apply(Config config) {
      return (exchange, chain) -> {
          try {
              String token = exchange.getRequest().getHeaders().get("Authorization").get(0).substring(7);
          } catch (NullPointerException e) {
              logger.warn("no token.");
              exchange.getResponse().setStatusCode(HttpStatus.valueOf(401));
              logger.info("status code :" + exchange.getResponse().getStatusCode());
              return chain.filter(exchange);
          }
          return chain.filter(exchange);
      };
  }
}
  • RouteLocator
@SpringBootApplication  
public class Gw2Application {
  public static void main(String[] args) {
      SpringApplication.run(Gw2Application.class, args);
  }

  @Bean
  public RouteLocator customRouteLocator(RouteLocatorBuilder builder, JwtRequestFilter jwtFilter) {

      return builder.routes()
              .route("path_route",  r-> r.path("/new")
                      .filters(f -> f
                              .filter(jwtFilter.apply(new JwtRequestFilter.Config("dummy", true, false)))
                              .addRequestHeader("Hello", "World")
                          .rewritePath("/new", "/"))
                          .uri("http://localhost:8083/"))
              .build();
  }
}

Reactor의 Mono와 Flux

Mono는 0-1개의 결과만을 처리하기 위한 Reactor의 객체이고, Flux는 0-N개인 여러 개의 결과를 처리하는 객체입니다. Reactor를 사용해 일련의 스트림을 코드로 작성하다 보면 보통 여러 스트림을 하나의 결과를 모아줄 때 Mono를 쓰고, 각각의 Mono를 합쳐서 여러 개의 값을 여러 개의 값을 처리하는 Flux로 표현할 수도 있습니다.

Spring Cloud Gateway에서 Reactor 프로젝트를 사용했다고 했는데, Reactor 프로젝트는 스프링 차원의 프레임워크는 아니고 비동기적으로 처리하기위한 별개의 프로젝트이다. Mono로 래핑한다, mono를 이용해 시그널을 보낸다..라고 하는데 이 개념이 아직 너무 생소하다. 결과를 Reactive하게 처리해주는 신호 객체 라고 생각하면 될 것 같다.

Filter로 ServerWebExchange의 Response에 적용한 Status가 제대로 나오지 않는 오류

exchange.getResponse().getHeaders().set("status", "401");
//exchange.getResponse().setStatusCode(HttpStatus.valueOf(401));

Header는 set 되면서 setStatusCode는 되지 않는다. 일단 Header로 처리하기로 하고 질문은 올려뒀다
https://stackoverflow.com/questions/59944589/can-i-conditionally-skip-other-filters-in-spring-cloud-gateway

Spring Cloud Gateway Cors Configuration

프론트엔드에서 직접 요청을 보내 Gateway의 동작을 확인하려니 어김없이 CORS 이슈에 걸렸다. WebFluxConfigurer 인터페이스를 implements해서 addCorsMappings 메소드를 오버라이드해서 사용했다. 이 방법 외에도 Filter를 직접 추가하거나 header를 직접 추가하는 등 방법은 여러가지이다.

추가: addCorsMappings가 잘 되다가 안돼서 RoutePredicateHandlerMapping 에 직접 추가하는 방식을 찾아내니까 5시간만에 성공했다 ㅠㅠ postman으로는 Access-Control-Allow-Origin header가 잘 나오는데 클라이언트로 직접 하면 왜 안나왔는지 아직도 도저히 모르겠다 

**그리고 인증 서버의 CORS 설정과 겹쳐 Access-Control-Allow-Origin : *,* 이런식으로 여러개가 나와서 서버에는 데이터가 잘 전송되지만 클라이언트단에서는 오류가 나는 문제가 있어 인증 서버의 CORS 설정을 해제했다.**

package com.quadcore.gw2.config;

import org.springframework.context.annotation.Configuration;
import org.springframework.web.reactive.config.CorsRegistry;
import org.springframework.web.reactive.config.EnableWebFlux;
import org.springframework.web.reactive.config.WebFluxConfigurer;

@Configuration
@EnableWebFlux
public class CorsGlobalConfiguration implements WebFluxConfigurer {
    @Override
    public void addCorsMappings(CorsRegistry corsRegistry) {
        corsRegistry.addMapping("/**")
                .allowedOrigins("*")
                .allowedMethods("*")
                .maxAge(3600);
    }
    
    @Bean
    public CorsConfiguration corsConfiguration(RoutePredicateHandlerMapping routePredicateHandlerMapping) {
        CorsConfiguration corsConfiguration = new CorsConfiguration().applyPermitDefaultValues();
        Arrays.asList(HttpMethod.OPTIONS, HttpMethod.PUT, HttpMethod.GET, HttpMethod.DELETE, HttpMethod.POST) .forEach(m -> corsConfiguration.addAllowedMethod(m));
        corsConfiguration.addAllowedOrigin("*");
        routePredicateHandlerMapping.setCorsConfigurations(new HashMap<String, CorsConfiguration>() {{ put("/**", corsConfiguration); }});
        return corsConfiguration;
    }
}
  • 인증 서버 프로젝트의 회원가입 Controller
  @PostMapping(path="/newuser/add")
    public Map<String, Object> addNewUser (@RequestBody Account account) {
        String un = account.getUsername();
        Map<String, Object> map = new HashMap<>();
        System.out.println("회원가입요청 아이디: "+un + "비번: " + account.getPassword());
        if (accountRepository.findByUsername(un) == null) {
            account.setUsername(un);
            account.setEmail(account.getEmail());
            if (un.equals("admin")) {
                account.setRole("ROLE_ADMIN");
            } else {
                account.setRole("ROLE_USER");
            }
            account.setPassword(bcryptEncoder.encode(account.getPassword()));
            map.put("success", true);
            accountRepository.save(account);
            return map;
        } else {
            map.put("success", false);
            map.put("message", "duplicated username");
        }
        return map;
    }
  • Gateway 프로젝트의 RouteLocator
@SpringBootApplication
public class Gw2Application {

    public static void main(String[] args) {
        SpringApplication.run(Gw2Application.class, args);
    }

    @Bean
    public RouteLocator customRouteLocator(RouteLocatorBuilder builder) {
        return builder.routes()
                .route("new_user",  r-> r.path("/new")
                        .filters(f -> f
                                .rewritePath("/new", "/newuser/add"))

                        .uri("http://localhost:8083/"))
                .build();
    }
}

FrontEnd -> Gateway -> 인증서버 -> MySQL 회원가입 완료

참고자료

https://www.baeldung.com/spring-webflux-cors

[https://www.baeldung.com/spring-webflux-cors)]
(Spring Webflux and CORS | Baeldung)

0개의 댓글