Spring Security를 사용하여 로그인, 로그아웃 시, DB에 히스토리 저장

수정이·2023년 1월 28일
0

Spring

목록 보기
16/16
post-thumbnail

요구사항

Spring Security의 세션을 사용하여 사용자의 로그인을 관리하는 프로젝트에서 로그인, 로그아웃 성공 시, DB에 해당 기록을 남기는 로직을 구현해야한다.

요구사항을 보고 처음 든 생각은 Handler를 사용해서 처리하면 되겠다고 생각하였다.
로그인 방법은 하나뿐이여서 Handler를 사용할 수 있겠지만, 로그아웃은 /logout을 호출하는 방법(=로그아웃 버튼을 사용)이 있고, 세션이 만료되어 로그아웃 처리되는 방법이 있다. 첫 번째 방법은 Handler를 사용하면 될 것 같았지만, 두 번째 방법은 Handler가 아닌 다른 방법을 찾아야할 것 같았다.


로그인 성공 시, DB에 저장

먼저 로그인 히스토리를 저장하는 로직부터 구현하기로 하였다. 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도 설정할 수 있다.


로그아웃 성공 시, DB에 저장

요구사항에 맞는 코드를 구현할 때, 거의 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안에는 삭제된 세션들이 저장된다. 그래서 그 안에 있는 로그아웃한 사용자의 정보를 가져올 수 있다.

이렇게 스프링 이벤트와 이벤트리스너를 사용하여 로그아웃 성공 시 히스토리를 저장할 수 있었다.

참고

사바라다 - 스프링 이벤트에 대해서 알아봅시다

profile
공부는 꾸준히... 글쓰기도 꾸준히...

0개의 댓글