사이드 프로젝트에서 Spring boot가 제공해주는 OAuth2.0 library를 통해 로그인을 구현하며 겪었던 문제들을 정리해 보겠습니다.
다중 서버로 scale out을 진행하고 로그인이 진행되지 않았습니다.
원인을 찾아보니 OAuth2.0은 세션을 사용하여 로그인이 완료될 때까지 로그인 프로세스의 상태를 임시로 저장하고 합니다.
[출처] - https://developer.okta.com/blog/2021/09/30/oauth-sessions-with-java
이 글을 보면서 들었던 생각은 'security를 설정하면서 sessionless 설정을 했는데?'
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
위의 설정은 로그인이 완료되고 로그인을 유지하기 위해 session을 사용할지의 설정입니다. session 자체를 막는 설정은 아니기 때문에 이 설정을 했다고 session을 사용하지 않는 건 아니었습니다.
세션을 사용한다는 사실을 알고 로그를 자세히 보니 아래와 같은 로그가 있었습니다.
2022-12-04 17:13:25.525 WARN 7780 --- [nio-8080-exec-2] o.a.c.util.SessionIdGeneratorBase : Creation of SecureRandom instance for session ID generation using [SHA1PRNG] took [261] milliseconds.
JWT 를 사용하며 session을 전혀 사용하지 않고있다고 생각했었습니다.
spring에서 간단한 의존성 빌드를 통해 사용할 수 있는 Spring-redis-session을 사용해 session을 클러스터링해 문제를 해결했습니다.
session을 왜 사용하는지 궁금해서 찾아봤습니다.
위 그림의 1번 요청을 처리하는 OAuth2AuthorizationRequestRedirectFilter 에 가보면 session이름을 가진 클래스가 2가지가 있습니다.
필터들 중 가장 마지막에 위치하고 인가를 처리하는 FilterSecurityInterceptor 에서 로그인하지 않은 사용자가 로그인이 필요한 리소스에 접근한다면, 예외를 발생 시킵니다.
ExceptionTranslationFilter는 인증 및 인가 예외를 처리하는 필터이고 FilterSecurityInterceptor 필터에서 발생한 예외를 처리합니다.
HttpSessionRequestCache 는 DefaultSaveRequest 객체를 세션에 저장하는 역할을 하며 DefaultSaveRequest는 현재 클라이언트의 요청 과정 중에 포함된 쿠키, 헤더, 파라미터 값들을 추출하여 보관하는 역할을 합니다.
이 과정 이후 클라이언트가 위에서 ExceptionTranslationFilter에 의해 이동된 로그인 페이지에서 인증을 하고 인증에 성공한 경우
인증 처리를 하는 인증 필터에서는 인증을 성공한 이후 처리를 진행하는 과정에서 HttpSessionRequestCache 클래스를 참조해서 이미 앞에서 세션에 저장되어 있는 DefalultSavedRequest를 얻어오고 클라이언트가 로그인 과정 이전 즉 처음에 접근하고자 했던 자원 주소 정보를 이 객체로부터 추출하여 그 페이지로 이동시켜 줍니다.
비로그인 상태로 로그인이 필요한 리소스 접근시, requset의 uri를 request로 부터 저장하고, response로 보내고 다시 request로 받아 로그인 완료후 원래 접근하려던 페이지로 보내줘 편의성을 높이는 과정을 session으로 secrurity에서는 이미 구현이 되어있는 것입니다.
HttpSessionRequestCache는 로그인 후 로그인 전 접근했던 uri로 redirect 해주기 위해 세션을 통해 저장합니다.
requset가 올바른 형식으로 왔다면 sendRedirectForAuthorization 메서드를 실행하고 session에 저장합니다.
this.authorizationRequestRepository.saveAuthorizationRequest(authorizationRequest, request, response);
HttpSessionOAuth2AuthorizationRequestRepository가 로그인 중간 과정을 세션에 저장하는 이유는 정확하게 찾지 못했지만,
세션의 사용 목적인 사용자의 상태를 저장해야 하는 이유가 있을거라고 생각합니다.
추가로 인증인가 예외를 처리하는 ExceptionTranslationFilter의 sendStartAuthentication 메서드에서 SecurityContextHolder에 emptyContext를 넣어주는 로직이 있어 예외가 발생한 상황에서 왜 emtyContext가 필요한지 알아봤습니다.
SecurityContextHolder에 현재 사용자 정보가 저장되어 있을 수 있고, 이 경우 로그인 페이지로 리디렉션하기 전에 SecurityContextHolder를 비워야만 인증 정보가 노출되지 않기 때문입니다.
protected void sendStartAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain,
AuthenticationException reason) throws ServletException, IOException {
// SEC-112: Clear the SecurityContextHolder's Authentication, as the
// existing Authentication is no longer considered valid
SecurityContext context = SecurityContextHolder.createEmptyContext();
SecurityContextHolder.setContext(context);
this.requestCache.saveRequest(request, response);
this.authenticationEntryPoint.commence(request, response, reason);
}
redirect uri를 back 서버 설정해서 사용합니다. 이 경우,
로그인 과정이 종료된 후, 생성된 JWT 토큰을 redirect로 front 서버로 전송해야 했습니다.
redirection으로 데이터를 전달하는 방법입니다.
front와 back에서 HTTPS인 SSL 통신을 하기 때문에 URL도 기본적으로 암호화 되어 전송되기 때문에 어느정도 안전하지만, HTTP 1.1 RFC에서는 권장하지 않습니다.
브라우저 히스토리, 캐시, 로그 파일 등에 남을 수 있으며, 보안 문제가 발생할 가능성이 있습니다.
하지만 JWT 에는 민감정보가 없기 때문에 이 방법을 사용해서 토큰을 반환했습니다.
참고