로그인 한 번으로 여러 서비스를 이용하는 것. Google 아이디 가지고 여러 서비스를 사용하는 것이 대표적인 예시인데 Oauth2와 아주 깊게 연관되어 있는 듯 하다. SSO라고 하면 '하나의 로그인으로 여러 서비스 이용하기'라는 컨셉 자체이고 Oauth는 그것을 실행하기 위한 대표적인 기술이라고 생각하면 될 것 같다. 우리 아키텍처에 적용한 MSA에서는 아예 다른 서비스를 사용하지는 않지만 사실은 아예 다른 기능 서버들을 하나의 로그인으로 사용한다는 점에서 내부적인 SSO이다.
앞서 만들었던 Gateway를 생각하지 않고 만들었던 단일 인증 서버에서는 들어오는 모든 요청에 대해 Request Filter로 필터링을 하고, Controller에 연결해 view page로 띄우는 MVC적 기능들까지 한번에 처리했었다. 하지만 Gateway를 나눈 지금은 직접 Controller를 사용하지 않고 MVC적인 기능을 인증 서버의 역할로 넘겨야 한다. JWT parsing&validating을 Gateway에서 GlobalFilter를 이용해 매 요청마다 처리하고, 인증 서버는 토큰 발행과 저장에 관한 일을 할 것이다. 지금 당장은 인증 서버가 하는 일이 적어보이지만 후에 Oauth2를 추가했을 때 resource 서버(Google과 같은 third party)에 대한 요청 처리도 인증 서버에서 할 계획이다.
출처: https://github.com/arawn/building-serverless-application-with-spring-webflux/blob/master/README.md
자바로 웹 기반 애플리케이션을 개발하기 위해서는 서블릿(Servlet)과 서블릿 컨테이너(Servlet container)를 사용한다. 물론 네트워크 및 스레드 제어, 요청과 응답 처리 등 바닥부터 쌓아올리면 서블릿과 서블릿 컨테이너를 사용하지 않고도 못만들 이유는 없지만 유지보수성이나 생산성 등을 생각해보면 비효율적이다. 그래서 자바 생태계에 웹 프레임워크(Web Framework)는 대부분 서블릿 위에 추상화 계층을 올리는 형태로 만들어져 있다. (Play Framework는 서블릿과 서블릿 컨테이너를 사용하지 않는다) 이런 상황에서 AWS Lambda가 서블릿 컨테이너 흉내를 내지 않으면, 웹 프레임워크들이 올바르게 동작하지 않을 것이 당연하다.
Vert.x도 서블릿과 서블릿 컨테이너 없이 HTTP 기반 애플리케이션을 만들 수 있으나, 성격이 다른 프레임워크라고 생각해서 배제했다.
한데, 마이크로서비스(Microservices)를 이어 리액티브(Reactive)라는 개념을 중심으로 자바 세상도 빠르게 변하고 있다. 바로 서블릿과 서블릿 컨테이너를 사용하지 않고, HTTP 기반 애플리케이션을 개발을 지원하는 Ratpack과 Spring WebFlux와 같은 프레임워크들이다.
Spring WebFlux는 Spring Framework 5에 포함된 새로운 웹 애플리케이션 프레임워크다. 기존 Spring MVC 모델에 비동기(asynchronous)와 넌블럭킹 I/O(non-blocking I/O) 처리를 맡기려면 너무 큰 변화가 필요했기 때문에 리액티브 프로그래밍(Reactive programming)을 지원하는 새로운 웹 프레임워크를 만들었다고 한다.
Spring MVC와 Spring WebFlux는 다음과 같은 구조로 되어 있다. (이후 MVC와 WebFlux라고 부르겠다)
MVC는 서블릿 컨테이너와 서블릿을 기반으로 웹 추상화 계층을 제공하고, WebFlux는 서블릿 컨테이너(Tomcat, Jetty) 외에도 Netty, Undertow와 같이 네트워크 애플리케이션 프레임워크 위에서 HTTP와 리액티브 스트림(Reactive Streams) 기반으로 웹 추상화 계층을 제공하고 있다.
MVC와 WebFlux 둘다 @Controller, @RequestMapping 형태에 @MVC 모델을 그대로 사용할 수 있다. WebFlux는 Route Functions이라는 함수형 프로그래밍 방식에 모델을 추가로 제공한다.
MVC는 다음과 같이 요청과 응답을 처리하고 있다.
서블릿 컨테이너로 들어온 요청이 DispatcherServlet에게 전달되면, DispatcherServlet는 순차적으로 HandlerMapping, HandlerAdapter에게 요청 처리를 위임하고, ViewResolver에게 응답 처리을 위임한다.
WebFlux도 크게 달라지진 않는다. 처리 흐름을 간단하게 도식화해서 보면 아래와 같다.
웹 서버(서블릿 컨테이너, Netty, Undertow)로 들어온 요청이 HttpHandler에게 전달되면, HttpHandler는 전처리 후 WebHandler에게 처리를 위임한다. WebHandler 내부에서는 HandlerMapping, HandlerAdapter, HandlerResultHandler 3가지 컴포넌트가 요청과 응답을 처리한다. 처리가 끝나면 HttpHandler는 후처리 후 응답을 종료한다. HandlerMapping, HandlerAdapter는 MVC가 사용하는 컴포넌트와 역할과 이름이 같지만, 동작 방식이 다르기 때문에 별도의 인터페이스를 사용한다.
Spring Cloud Gateway is built on Spring Boot 2.x, Spring WebFlux, and Project Reactor. As a consequence, many of the familiar synchronous libraries (Spring Data and Spring Security, for example) and patterns you know may not apply when you use Spring Cloud Gateway. If you are unfamiliar with these projects, we suggest you begin by reading their documentation to familiarize yourself with some of the new concepts before working with Spring Cloud Gateway.
Spring Cloud Gateway requires the Netty runtime provided by Spring Boot and Spring Webflux. It does not work in a traditional Servlet Container or when built as a WAR.
Spring Cloud Gateway 는 WebFlux와 Reactor 프로젝트를 기반으로 비동기적으로 만들어졌다. 그리고 Servlet 대신 netty 서버를 이용한다고 한다. 그동안 사용하던 Spring MVC에서의 servlet 기반 프로젝트와는 아예 다른 개념인 것이다...이제 막 익숙해졌는데 ㅎㅎ.. 마이크로서비스를 위한 API Gateway는 늘어난 http통신을 빨리빨리 처리하기위해 nonblocking & aysnchronus 하게 돌아갈 필요가 있다. Fully Reactive한 프로그램을 만드려면 다른 마이크로 서비스들도 WebFlux로 짜여져야 하는 듯하지만 우리는 각자 다른 프레임워크를 사용해 각자의 기능을 만드므로 다른 프로그램은 스프링이 아닐수도 있다는 전제를 기반으로 스프링에 의존적이지 않게 Gateway 단에서의 최선을 다해보려고 한다.
고성능 프로토콜 서버와 클라이언트를 신속히 개발하기 위한 비동기식 이벤트 기반 네트워크 애플리케이션 프레임워크이다. 내가 네트워크 어플까지 만들 수는 없으니 일단 동작 방식만 살짝 공부하고 넘어간다.
기존 Socket I/O 를 이용해 통신을 하면 bind - listen - accept와 같은 일들을 했어야했는데(그리고 내가 직접 해본 유일한 네트워크 통신인데) 이 소켓을 사용하려면 thread를 생성해서 관리해야 하고, 그러면 어떤 소켓에서 이벤트가 일어났는지 모르게 되고 접속이 많아질수록 자원을 낭비하게 된다. 그래서 Client의 커넥션 수립마다 thread를 생성하지 않아도 되는 Java NIO(Non-blocking IO)가 나왔다고 한다. 그렇다고 Thread를 아예 하나만 사용하는 게 아니라 알아서 적절히 관리를 해준다고 한다. Spring이 원래 HTTP Servlet을 다루기 위한 프레임워크였지만 Netty가 Spring의 Singleton 방식의 Bean을 지원한다고 한다. 아직 완전히 이해된건 아니지만 Spring Cloud Gateway가 비동기적으로 요청을 처리하기 위해 네트워크 애플리케이션까지 완전히 비동기적인걸로 사용하는 듯하다.
그런데 Spring Security에서 인증을 받고 넘어가려면
SecurityContextHolder.getContext().setAuthentication(authencation객체)
식으로 받아야 하고, 이 authentication 객체는 보통 UsernamePasswordAuthenticationToken usernamePasswordAuthenticationToken = new UsernamePasswordAuthenticationToken( userDetails, null, authorities)
형태로 만들어진다는 것인데... 여기서 userDetails를 만드려면
UserDetails userDetails = User.builder().username(String.valueOf(parseInfo.get("username"))).authorities(rolesCollection).password("dummy").build();
이런식으로 만들게 된다. 문제는 UserDetails를 이렇게 마음대로 build하려면 username, password, authorities 세 개 모두가 필수로 필요하다는 것이다. 나는 한 번 발급한 JWT에 대해 매번 DB에 접근해서 password를 받기 싫은데. 그래서 이전에는 userDetails의 비밀번호를 더미로 주고 만들었었다. 이번에 뭔가 해결책을 얻고 싶어서 스택오버플로우에 일단 질문을 올렸다.
The Spring Cloud Security module provides features related to token-based security in Spring Boot applications.Specifically, it makes OAuth2-based SSO easier – with support for relaying tokens between Resource Servers, as well as configuring downstream authentication using an embedded Zuul proxy.
Spring Security는 그 자체로 하나의 라이브러리고 Spring Cloud Security는 위의 상황에 최적화되어있는 Spring Cloud 생태계의 또다른 라이브러리인 듯 하다. Spring Cloud Security는 MSA 구조에서 Spring boot client에 Rest api로 OAuth2 SSO 인증서버 만들기에 최적화 된 라이브러리이므로 우리 프로젝트에서 OAuth2를 쓴다면 꼭 검토해봐야 할 듯 하다. 아직은 아닌듯
Spring @EnableWebFluxSecurity를 이용하면 Webflux를 이용할 Gateway 단에서도 @EnableWebSecurity로 관리했듯 Spring Security의 기능들을 사용할 수 있다. UserDetails의 정보 확인이나 라우팅 별 권한 설정 등을 편하게 할 수 있지만 JWT를 사용할 때는 오히려 불편한 것 같다. 게이트웨이에는 일단은 둘 다 적용하지 않을 계획이다. 매 요청마다 DB에 들러 확인하는 과정이 있다면 UserDetailsService를 사용하는 것도 나쁘지 않으나 그건 내가 바라는 방식이 아니다. 따라서 이번에는 security가 아닌 커스텀 GlobalFilter를 만들고 거기서 매 요청마다 JWT를 검사할 것이다. 후에 업그레이드 하는걸로
https://cloud.spring.io/spring-cloud-gateway/reference/html/
https://gompangs.tistory.com/entry/Netty-%ED%86%B5%EC%8B%A0-%EC%84%9C%EB%B2%84-%EA%B0%9C%EB%B0%9C-%EA%B4%80%EB%A0%A8-%EC%A3%BC%EC%A0%80%EB%A6%AC
잘 봤습니다~