AS-IS | TO-BE |
User Service는 User Repository에 직접 접근하는 서비스로, 클라이언트가 곧바로 User Service와 통신하는 것은 보안상 좋지 않습니다. 그러므로 클라이언트와 User Service 사이에 Auth Service를 만들어서 인증 및 권한 처리를 수행하도록 해야 합니다.
1. AuthService API
User Service는 깊은 마이크로서비스로서 저장소에 접근하는 작업만 수행하고, Auth Service가 사용자에 대한 인증과 권한에 대한 작업을 수행합니다. 현재 인증에 대한 기능만 필요하므로 Spring Session만 추가하여 작업하고, 추후에 권한에 대한 기능을 추가할 예정입니다.
WebClient vs RestTemplate
WebClient와 RestTemplate 모두 서비스 간 통신을 위해 사용할 수 있으나, 차이점이 존재합니다.
RestTemplate : 블럭킹으로 동작하므로 Reactive에는 적절하지 못합니다. 이를 해결하기 위해 Spring 4에서 AsyncRestTemplate이 등장하였으나, Spring 5가 등장하면서 Deprecated 되었습니다.
WebClient : Spring 5와 함께 등장하였고, 논블럭킹, 비동기로 동작합니다.
2. Spring Session 설정
사용자 인증에 대한 처리를 할 때에는 크게 웹토큰 방식과 세션 방식을 사용합니다. 우리는 세션 방식을 사용할 것이며, 마이크로서비스 간에 세션 공유가 되어야 하므로 Spring Session을 이용합니다.
세션을 저장하기 위해서는 저장소가 필요한데, 보통 Redis를 많이 사용합니다. 하지만 이번에는 MongoDB를 사용하였습니다.
Web MVC에서는 HttpSession을 사용하므로 DefaultCookieSerializer라는 Custom Cookie를 만들어 주는 빈을 사용하면 되지만, WebFlux에서는 이를 사용할 수 없습니다. WebFlux가 내부적으로 WebSession이라는 것을 이용하기 때문입니다. 그러므로 WebSessionIdResolver라는 빈을 사용하여야 합니다. WebSession은 Cookie방식과 Header방식 두 가지로 나뉘는데, 마이크로서비스에서는 CORS 요청으로 인해 쿠키에 저장할 수는 없습니다. 그러므로 Header에 X-SESSION-ID라는 이름으로 Request 헤더에 직접 넣어주었습니다.
기본적으로 브라우저는 Header 정보에 대한 접근이 불가합니다. 네트워크상에서는 정보가 보이지만, 클라이언트단에서 해당 헤더 정보에 접근하려고 할 경우 데이터를 가져올 수 없습니다. 그러므로, CORS 설정에서 exposedHeaders 설정을 추가해주어야 합니다.
[WebFluxConfig]
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/**").allowedOrigins("*")
.allowedMethods("*").allowCredentials(true).allowedHeaders("*").exposedHeaders(HttpSessionConfig.X_SESSION_ID);
}
현재 Auth Service는 세션을 위한 MongoDB 하나만을 사용하고 있으나, 추후 Auth를 위한 DB가 필요할 경우 Auth Service에 대해 멀티 데이터베이스 설정에 대한 부분이 필요할 것으로 보입니다.
3. HandlerAdapter 설정
로그인이 필요한 서비스 요청에 대해서는 어노테이션을 만들어 인터셉터로 처리하도록 만들어야 합니다. 그런데 WebFlux의 경우에는 HandlerInterceptor가 아닌 HandlerAdapter를 통해 이 서비스를 구현해야 합니다.
[SessionHandler]
@Component
public class SessionHandler extends RequestMappingHandlerAdapter {
@Override
public boolean supports(Object handler) {
if (handler instanceof HandlerMethod) {
HandlerMethod hm = (HandlerMethod) handler;
return hm.hasMethodAnnotation(LoginRequired.class);
}
return false;
}
@Override
public Mono<HandlerResult> handle(ServerWebExchange exchange, Object handler) {
HandlerMethod hm = (HandlerMethod) handler;
return exchange.getSession().flatMap(session -> {
if (!session.getAttributes().containsKey(AuthController.LOGGED_IN_USER)) {
Object response = Mono.just(ResponseEntity.status(440).body(new ResponseMessage("로그인하지 않았거나, 세션이 만료되었습니다.")));
return Mono.just(new HandlerResult(handler, response, hm.getReturnType()));
}
return super.handle(exchange, handler);
});
}
}
이 때 유의해야 할 점은, CORS 요청의 경우 OPTION 요청을 먼저 보내 CORS가 가능한 서버인지 확인하기 때문에, 이에 대한 처리를 support에서 해주어야 한다는 점입니다.
option요청인 경우에는 인터셉팅되지 않고 실제 요청이 들어왔을 때만 수행되어야 하므로, 들어온 handler가 handlerMethod 타입인지를 확인해주어야 합니다.
로그인 체크에 대한 인터셉터 처리는 Auth Service만 수행하는 작업이 아닌, 다른 마이크로서비스에서도 수행할 수 있는 작업입니다. 그러므로 추후에 기능이 추가될 경우에는 Common으로 옮겨주어야 할 것으로 보이며, 이 경우에는 Spring Boot가 자동으로 컴포넌트 스캔을 해주지 않기 때문에 따로 component 스캔으로 추가해주어야 합니다.
4. Angular 설정 변경
기존에는 AuthService가 없었기 때문에, Auth에 대한 처리를 앵귤러 어플리케이션에서 응답을 받아 처리하였습니다. 이제 Auth Service가 생겼으므로 이 코드를 모두 제거하였습니다. 앵귤러 애플리케이션은 결과를 받아, 정상이면 데이터 매핑을, 에러이면 에러 다이얼로그를 띄우기만 하면 됩니다.
또한 세션에 대한 기능이 추가되었으므로 앵귤러 어플리케이션에서도 역시 이에 대한 처리가 필요합니다.
요청을 보내기 전 인터셉터에서 로컬스토리지에 있는 세션 정보를 리퀘스트 헤더에 담아 보내며, 응답을 받은 경우 리스폰스 헤더에 있는 세션정보를 로컬 스토리지에 저장합니다.
[references]
'MSA (Micro Service Architecture) > Simple MSA Prototyping' 카테고리의 다른 글
4. Service Gateway (0) | 2020.10.19 |
---|---|
2. 실시간 데이터 동기화 (0) | 2020.10.16 |
1. UserService 구현 (0) | 2020.10.15 |
댓글