Spring Cloud Gateway, Eureka 실무적용 후기(2)
사실 이런 것을 회사에 적용할때는
독립적으로 실행했을떄도 완벽히 동작해야한다는 선행조건이 있다.
3. Spring Cloud Gateway적용
이는 좀 어려움이 있었다.
일단 요청을 받고 전처리를 하여 요청을 Eureka에게 요청하여 넘겨줄것인지 단순 라우팅 작업만 필요한지를 판단해야한다.
만약 전자라면 API콜을 한 번 더 해야하는것인데 그에 따른 컨트롤러 구성을 해줘야한다.
옛날 블로그 글들을 보니, Spring Cloud Gateway(SCG)가 동기식으로 돌아가고 있었고, 현재는 비동기로 돌아가다보니 옛날 설정으로 프로젝트를 구성하면
ServerCodecConfigurer'
가 없다느니 등의 오류를 만날 수 있었다.
현재는 implementation 'org.springframework.boot:spring-boot-starter-web'
라이브러리를 쓰는게 아닌, web-flux
라이브러리를 가져다 써야한다.
근데 필자는 단순 라우팅 + 인증인가 단계만 필요하므로 컨트롤러를 제작할 이유는 없다,
1
2
3
4
5
6
7
8
9
10
11
12
@Configuration
public class FilterConfig {
@Bean
public RouteLocator routeLocator(RouteLocatorBuilder builder) {
return builder.routes()
.route(r -> r.path("/api/v1/iparking/**")
.filters(f -> f.addRequestHeader("iparking-request", "iparking-test")
.addResponseHeader("iparking-response","iparking-response-value"))
.uri("lb://iparking")).build();
}
}
이렇게 작성해주면 유레카에 등록된 서비스를 찾아서 요청을 제대로 보내는 것을 확인할 수 있다.
yml
로 작성안하고 코드로 작성한 이유는 단순하다.
yml
이 길어지는게 싫었고, 문제가 생길 수 추적이 어려울 것 같았다. 또한 어차피 배포 해야한다면 코드 수정해서 배포하는게 내 입장에서는 낫지 않을까 싶어서 코드로 작성하였다.
필터 적용
위에서는 필터 예제로 아무 코드나 넣은 것이고,
실제 인증 및 인가처리를 하는 코드를 삽입하면 완성될 것 같다.
전역 라우트에 적용되는 Global Filter와 특정 라우트에 적용되는 Custom Fileter가 있는데, 우리 팀의 주요서비스에 적용된 인증을 간단히 구현한 것을 Global Filter로 놓고, 고객사들에게 개별적으로 적용해야하는 필터들은 Custom Filterf로 넣으면 될거라 생각했다.
Global 필터제작
인증관련 필터는 쿠키에서 값을 가져와서 해당 쿠키에 저장된 값이 유효한지에 대한 로직이다.
보안상 코드를 전부 보여줄 순 없으며,
1
2
3
4
5
6
7
8
9
10
private User getUserFromCookie(ServerHttpRequest request) {
String cookie = HttpRequestUtils.getCookieValue(request, cookie);
if (StringUtils.isBlank(cookie)) {
return null;
}
User user = new OcUser();
String token = jwtTokenUtil.getUserFromToken(cookie);
log.info("token: {}", token);
return null;
}
이런식으로 구성될 것이다.
토큰값이 올바르면 제대로된 응답을 줄 것이고, 토큰 값이 올바르지 않다면 403 Forbidden을 반환하게 하였다.
Custom 필터 제작
Custom필터에는 고객사의 유저가 맞는지에 대한 추가적인 필터를 제작하면 된다.
우리의 고객사인 Iparking 기준으로, 해당 유저가 Iparking 사용자가 맞는지에 대한 로직을 추가하면 된다.
Custom필터는 Global필터와 다르게 접근 유저가 정말 이 서비스에 접근할 수 있는지와, dev, real, beta등의 환경에 따라 접근 권한 체크 여부를 다르게 해줘야하므로 관련 코드를 넣어야한다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
@Slf4j
@Component
public class IparkingFilter extends AbstractGatewayFilterFactory<IparkingFilter.Config>{
// 환경체크 서비스
private final EnvService envService;
public IparkingFilter(EnvService envService){
super(Config.class);
this.envService = envService;
}
@Override
public GatewayFilter apply(Config config) {
// Custom Pre Filter
// exchange, chain 객체 받아.
return (exchange, chain) -> {
ServerHttpRequest request = exchange.getRequest();
ServerHttpResponse response = exchange.getResponse();
UserAffiliationDto affiliation = UserContextHolder.getAffiliation();
if((envService.isDev() || envService.isAlpha())&& !(체크로직))){
return Mono.error(new ResponseStatusException(HttpStatus.FORBIDDEN));
}
// Custom Post Filter
//비동기 방식으로 지원할 때 단일값 Mono 타입을 전달.
return chain.filter(exchange).then(Mono.fromRunnable(()-> {
log.info("Custom POST filter : response code -> {}", response.getStatusCode());
}));
};
}
public static class Config{
//Configuration
}
}
코드를 보면 먼저, 커스텀으로 제작한 컨택스트 홀더에서 유저정보를 가져와 그 밑에서 환경에 따라 접근 여부를 채크하고 있다.
실제 체크하는 로직은 많으나, 간소화하여 작성하였다.
이런 방식이 가능한 이유는 global 필터에서 토큰을 검사하고 컨택스트 홀더에 관련한 값을 넣어서 가능한 일이다.
global 필터에서는 토큰 검증만 하고, custom 필터에서는 해당 토큰에 저장된 유저정보가 접근하려는 서비스에 접근 가능한지 여부를 확인하는 것이다.
후기
글은 짧으나 꽤나 해맸다.
필터 적용하느라고 부수적인 코드들을 작성해줄게 많았다. 유틸적 코드나 전역 에러나 상수 등.. 그리고 Mono
라는 개념을 실제로는 처음 사용해보아서 이 부분에서도 애먹었던 것 같다.
그 외에는 차분히 진행하니 되었다.
글은 여기서 끝나지만 나머지는 내가 실무적으로 해결해야할 문제들이므로.. 여기서 마무리 지어야할 것 같다.