먼저 라우팅을 담당할 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에서 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
ServerHttpRequest getRequest()
Return the current HTTP request.
ServerHttpResponse getResponse()
Return the current HTTP response.
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를 이용해 직접 필터를 걸어줘야 한다.
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);
}
}
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);
};
}
}
@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();
}
}
Mono는 0-1개의 결과만을 처리하기 위한 Reactor의 객체이고, Flux는 0-N개인 여러 개의 결과를 처리하는 객체입니다. Reactor를 사용해 일련의 스트림을 코드로 작성하다 보면 보통 여러 스트림을 하나의 결과를 모아줄 때 Mono를 쓰고, 각각의 Mono를 합쳐서 여러 개의 값을 여러 개의 값을 처리하는 Flux로 표현할 수도 있습니다.
Spring Cloud Gateway에서 Reactor 프로젝트를 사용했다고 했는데, Reactor 프로젝트는 스프링 차원의 프레임워크는 아니고 비동기적으로 처리하기위한 별개의 프로젝트이다. Mono로 래핑한다, mono를 이용해 시그널을 보낸다..라고 하는데 이 개념이 아직 너무 생소하다. 결과를 Reactive하게 처리해주는 신호 객체 라고 생각하면 될 것 같다.
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
프론트엔드에서 직접 요청을 보내 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;
}
}
@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;
}
@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();
}
}
https://www.baeldung.com/spring-webflux-cors
[https://www.baeldung.com/spring-webflux-cors)]
(Spring Webflux and CORS | Baeldung)