Spring Security의 세션을 사용하여 사용자의 로그인을 관리하는 프로젝트에서 로그인, 로그아웃 성공 시, DB에 해당 기록을 남기는 로직을 구현해야한다.
요구사항을 보고 처음 든 생각은 Handler를 사용해서 처리하면 되겠다고 생각하였다.
로그인 방법은 하나뿐이여서 Handler를 사용할 수 있겠지만, 로그아웃은 /logout
을 호출하는 방법(=로그아웃 버튼을 사용)이 있고, 세션이 만료되어 로그아웃 처리되는 방법이 있다. 첫 번째 방법은 Handler를 사용하면 될 것 같았지만, 두 번째 방법은 Handler가 아닌 다른 방법을 찾아야할 것 같았다.
먼저 로그인 히스토리를 저장하는 로직부터 구현하기로 하였다. DB에 저장되는 내용은 로그인한 사용자의 정보와 로그인인지 로그아웃인지 구별하는 코드와 시간이다.
Spring Security는 로그인이 성공했을 때, 핸들러를 호출하여 원하는 로직을 처리하고, API를 호출할 수 있다.
//SecurityConfig.java
...
http.formLogin(form -> form
.loginPage("/login")
.successHandler(new CustomLoginSuccessHandler(loginHistService))
...
위 코드와 같이 successHandler()
안에 핸들러를 구현한 클래스를 넣어주면 된다. 그러면 로그인을 성공하였을 때, 설정한 클래스 속의 로직을 실행시킨다.
이제 DB를 저장하는 서비스 로직을 호출하는 핸들러를 만들어보자.
@Component
@RequiredArgsConstructor
public class CustomLoginSuccessHandler implements AuthenticationSuccessHandler {
private final LoginHistervice loginHistService;
@Override
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException {
// authentication에서 로그인한 사용자의 정보를 가져온다.
User user = (User) authentication.getPrincipal();
loginHistService.saveLoginHist(user.getUsername(), CommonUtil.getClientIP(), "LOGIN");
response.sendRedirect("/index");
}
}
로그인 성공 핸들러를 만들기 위해서는 AuthenticationSuccessHandler
를 구현해야한다. 그리고, onAuthenticationSuccess
메소드안에 원하는 로직을 구현하면 된다. 나는 로그인 히스토리를 저장하는 코드를 따로 구현하였기 때문에 loginHistService
를 의존받아서 처리해주었다.
saveLoginHist
에 대해 간단히 설명하자면, 로그인한 사용자의 username과 사용자의 IP 그리고 로그인인지 로그아웃인지 구별해주는 코드를 받아 DB에 저장하는 메소드이다.
onAuthenticationSuccess
는 HttpServletRequest와 Response를 매개변수로 받기 때문에 로그인 후에 호출할 API도 설정할 수 있다.
요구사항에 맞는 코드를 구현할 때, 거의 90%의 시간을 소요한 부분이 로그아웃 부분이다. 핸들러를 통해서 2가지의 로그아웃 방법을 잡을 수 있다면 좋았겠지만, 아쉽게도 /logout
을 호출하는 방법만 핸들러를 통해 해결할 수 있었다.
세션이 만료됐을 때도 히스토리를 남기기 위해서 구글에 열심히 찾아본 결과, 두 가지 방법 모두 세션을 삭제시킨다는 공통점이 있었고, 세션이 삭제됐을 때 작동하는 이벤트가 있다는 것을 알게되었다.
(참고로 여기서 이벤트와 이벤트리스너에 대해서 설명하지 않을 것이다.)
먼저 세션이 삭제됐을 때 작동하는 이벤트를 실행시켜줄 이벤트 리스너를 Servlet 컨테이너에 추가로 등록하기 위해서 해당 코드를 SecurityConfig
에 등록해주었다.
@Bean
public ServletListenerRegistrationBean<HttpSessionEventPublisher> httpSessionEventPublisher() {
return new ServletListenerRegistrationBean<>(new HttpSessionEventPublisher());
}
그리고 SessionDestroyedEvent
를 매개변수로 받는 메소드를 가진 클래스를 생성해주었다.
@RequiredArgsConstructor
public class SessionDestroyedHandler {
private final LoginHistService loginHistService;
@EventListener
public void onSessionDestroyEvent(SessionDestroyedEvent event) {
List<SecurityContext> securityContexts = event.getSecurityContexts();
if (securityContexts != null) {
for (SecurityContext securityContext : securityContexts) {
User user = (User) securityContext.getAuthentication().getPrincipal();
loginHistService.saveLoginHist(user.getUsername(), CommonUtil.getClientIP(), "LOGOUT");
}
}
}
}
이 클래스는 로그인 핸들러처럼 동작을 하지만, 상속을 받지 않고 동작한다는 것이 다르다.
@EventListener
어노테이션을 붙여줌으로써 해당 메소드는 이벤트리스너로 동작한다. 무슨말이냐면 세션이 삭제되면 SessionDestroyedEvent
이벤트가 동작하고 그 이벤트가 동작하면 onSessionDestroyEvent
메소드가 동작한다는 소리이다.
event안에는 삭제된 세션들이 저장된다. 그래서 그 안에 있는 로그아웃한 사용자의 정보를 가져올 수 있다.
이렇게 스프링 이벤트와 이벤트리스너를 사용하여 로그아웃 성공 시 히스토리를 저장할 수 있었다.