Spring Security : SessionManagementFilter / ConcurrentSessionFilter

    이 글은 인프런 정수원님의 강의를 복습하며 정리한 글입니다.

     

    SessionManagementFilter

    앞선 게시글에서 http.sessionManagement()를 이용해서 Spring에서 사용하는 여러 세션에 대한 정책을 설정했었다. 이 API를 이용해서 처리한 것들을 정리해보면 다음과 같다.

    • 동시 세션 제어 : 동시 접속 관련 처리
    • 세션 고정 보호 : 세션 고정 공격으로부터 보호
    • 세션 관리 : 인증 시, 사용자의 세션 정보를 등록, 조회, 삭제 등 세션 이력 관리
    • 세션 생성 정책 설정 : 세션 생성 정책을 조작 

    위의 기능은 SpringSecurity에서 어떤 필터가 지원을 해줄까? 바로 SessionManagementFilter가 위의 네 가지 기능을 지원해준다.  SessionManagementFilter는 CompositeSessionAuthStrategy 클래스를 이용해 위의 4가지에 대응해준다. CompositeSessionAuthStrategy는 내부적으로 위임 전략 4가지 정도를 가지고 있고, API 설정에 따라 For문을 돌면서 해당 기능을 처리해준다. 

     

    ConcurrentSessionFilter

    ConcurrentSessionFilter는 동시 세션을 관리하는 필터다. 사용자의 요청이 들어올 때 마다, 이 필터는 매번 세션이 만료되었는지를 살펴본다. 세션이 만료되었을 경우, 세션을 즉시 만료하고 현재 요청자를 로그아웃 시킨 후, 세션 만료가 되었다는 메시지를 사용자에게 응답해주는 역할을 한다. 물리적으로 세션을 만료시키는 역할을 한다. 

     

    동시 세션 관리 : SessionManagementFilter + ConcurrentSessionFilter

    앞서 이야기했던 것처럼 SessionManagementFilter와 ConcurrentSessionFilter는 모두 동시 세션을 제어하는 역할을 한다. 각각 어떤 역할을 하는지 아래 그림에서 확인이 가능하다. 

    1. 사용자가 인증 요청을 한다.
    2. 인증 요청을 하는 과정에서 SessionManagementFilter로 넘어온다. SessionManagementFilter는 동일 계정의 세션 갯수를 살펴본다. 
    3. 세션 갯수가 초과된 것을 확인하였다. 현재 사용자는 로그인 처리를 해주고, 이전 사용자의 세션을 session.expireNow()를 이용해 만료처리했다.  이 때, 실제 물리적인 세션이 만료되는 것이 아닌 Session Info의 Expired 변수가 만료된다.
    4. 이전 사용자가 특정 자원에 접근 요청할 때, ConcurrentSessionFilter에서 세션 만료를 검사한다. 이 때, ConcurrentSessionFilter는 SessionManagementFilter에서 expired.Now()를 했는지 isExpired() 메서드를 이용해 확인한다. 이 때, Session Info를 확인하고, 물리적으로 Session을 만료처리한다. 
    5. 세션이 만료된 경우 즉시 Logout 해버리고 오류 페이지를 응답해준다. 

    위의 과정으로 처리가 되는데, 각 필터마다 주된 역할을 나눠보면 좀 더 보기 편하다. SessionManagementFilter는 인증하는 과정에서 동시 세션을 확인하고, 필요시 세션을 만료처리해준다. 그리고 ConcurrentSession은 세션이 만료되었는지 확인하고, 만료된 경우 Logout을 처리 및 오류 페이지를 응답해주는 역할을 한다. 

     

    전체 흐름 이해하기

    1. 사용자1이 로그인을 요청한다. 이 때, UsernamePasswordAuthFilter에서 인증처리가 완료된다. 인증처리가 완료되면 ConcurrentSessionControlAuthStrategy 클래스를 호출한다. 
    2. ConcurrentSessionControlAuthStrategy 클래스는 동시 세션을 관리하는 클래스다. 이 클래스는 현재 인증 처리된 계정으로 생성된 세션이 몇개인지를 확인한다. 사용자1이 처음 로그인하는 것이기 때문에 동시 세션 수는 0이 된다. 
    3. 이후, ChangeSessionIdAuthStrategy 클래스를 호출한다. 이 클래스는 세션 고정 보호를 하는 클래스인데, session.changeSesionId()이다. 세션이 없다면 여기서 새로운 세션을 생성하고, 새로운 세션 쿠키를 발급한다. 
    4. RegisterSessionAuthStragety 클래스를 호출한다. 이 클래스는 사용자의 세션 정보를 등록 및 저장하는 역할을 한다. 세션 정보가 등록되기 때문에, 이 계정에 대한 동시 세션의 수는 1이 된다. 즉, 사용자1이 로그인하면 최대 세션 갯수인 1이 된다. 
    5. 사용자2가 동일하게 인증 요청을 하면 ConcurrentSessionControlAuthStrategy로 간다. 로그인 계정에 대한 세션을 확인했을 때, Session Count가 1인 것을 확인한다. 
    6. Maximum Session Count를 초과했다. 이 때, 인증 실패 전략을 사용할 경우 SessionAuthException을 발생시킨다. 따라서 뒷쪽에 있는 ChangeSessionIdAuthStrategy, RegisterSessionAuthStrategy는 호출되지 않는다.
    7. 세션 만료 전략인 경우, 사용자2는 인증에 성공한다. 그리고 session.expireNow()를 이용해 사용자1의 세션을 만료시킨다. 이제, 서버에는 사용자1, 사용자2의 세션이 모두 생성되어있는 상황이다. 
    8. 사용자2는 creationSessionAuthStrategy로 가서 SessionId를 바꾸고, RegisterSessionAuthStrategy로 가서 현재 세션 정보를 서버에 저장한다. 이 때, 세션 정보를 등록 완료하는 순간 Session Count는 2가 된다. 
    9. 사용자1이 다른 자원에 접근할 경우 ConcurrentSessionFilter(매 요청마다 확인)가 사용자1의 세션 만료 여부를 확인한다. (isExpired()). 이 때, ConcurrentSessionControlAuthStrategy의 session.expireNow()로 이동해서 확인한다. 
    10. 세션 만료 체크를 확인했기 때문에 사용자1의 세션을 바로 Logout 처리하고, 응답으로 Session Expired exception을 보내준다. 

     

     

    코드로 알아보기

    이번 코드에서는 사용자1이 로그인 하고, 사용자2가 동일한 계정으로 로그인 했을 때 발생하는 과정들을 살펴보려고 한다. 설정 클래스는 아래와 같이 작성했다. 

    http
            .authorizeRequests()
            .antMatchers("/user").authenticated()
            .anyRequest().permitAll();
    
    http
            .formLogin();
    
    http
            .sessionManagement()
            .sessionFixation()
            .changeSessionId()
            .maximumSessions(1)
            .maxSessionsPreventsLogin(true);
    
    http
            .sessionManagement()
            .sessionCreationPolicy(SessionCreationPolicy.ALWAYS);

     

    AbstractAuthenticationProcessingFilter

    사용자1이 로그인을 시도하는 경우, UsernamePasswordAuthenticationFilter를 통해서 인증을 처리해서 인증 객체를 만든다. 그리고 그 인증 객체는 AbstractAuthenticationProcessingFilter로 전달되는데, 이 클래스에서 sessionStrategy.onAuthentication을 호출한다. 

    CompositeSessionAuthenticationStrategy.onAuthentication()

    CompositeSessionAuthStrategy 클래스로 넘어온다. 이 클래스는 내부적으로 delegateStrategy를 리스트 형태로 가진다.

    CompositeSessionAuthenticationStrategy.delegateStrategies

    전략이 총 4개가 있는 것을 확인할 수 있고, 4개를 For문을 돌면서 실행을 한다. 따라서 ConcurrentSessionControlAuthStrategy에 인증 객체 / Request / Response를 넘겨서 처리를 해준다. 

    ConcurrentSessionControlAuthStrategy

    ConcurrentSessionControlAuthStrategy로 넘어온다. 먼저 인증 객체를 넘겨주면서 현재 이 계정에 대한 최대 허락 세션 수가 얼마인지를 확인한다. 이 때, 허락 세션이 -1인 경우, 무제한으로 접근이 가능하기 때문에 Return 해준다.

    ConcurrentSessionControlAuthStrategy

    세션 갯수가 제한된 경우이기 때문에 sessionRegistry.getAllSessions에 인증 객체의 principal을 넘겨서 현재 계정으로 등록된 모든 세션을 가져온다. 이 때, Principal은 ID / 비밀번호 / 권한정보 등이 기록된 객체다. 처음 로그인이기 때문에 SessionCount가 당연히 allowedSessions보다 작기 때문에 Return 한다. 

    ConcurrentSessionControlAuthStrategy

    다시 ConcurrentSessionControlAuthStrategy로 넘어와서 For문을 돌면서 다음 전략을 처리해준다. 이 다음으로 넘어갈 Strategy는 ChangeSessionIdAuthenticationStrategy다. 왜냐하면 앞에서 세션 고정 보호 전략으로 changeSessionId()를 설정했기 때문이다.

    AbstractSessionFixationProtectionStrategy

    AbstractSessionFixationProtectionStrategy로 넘어와서 Lock 처리를 한 후 현재 세션에서 Session Id만 바꾸는 것을 확인할 수 있다. 

    CompositeSessionAuthenticationStrategy

    다시 CompositeSessionAuthenticationStrategy로 돌아와서 다음 위임 정책으로 넘어간다. 이 때 넘어가는 정책은 RegisterSessionAuthStrategy다.

    RegisteSessionAuthStrategy

    RegisterSessionAuthStrategy로 넘어온다. 넘어와서 현재 Session Id와 인증 객체의 정보를 넘겨줘서 세션을 등록해준다. 이 때 registerNewSession() 메서드를 좀 더 타고 가봤다.

    SessionRegistryImpl

    SessionRegistryImpl 클래스로 넘어온다. 이 클래스에 넘어와서 sessionId와 principals의 Map 구조에 값을 넣어준다.

    값을 확인해보면 다음과 같이 principals라는 Map에 User 객체를 key로 해서 Value로 Session Id가 들어간 것을 볼 수 있다. 즉, 특정 User 정보에 대해서 Session이 몇개 들어갔는지를 체크할 수 있게 될 것이다. 

    ConcurrentSessionControlAuthStrategy

    두번째 시도 시, 최대 동시 세션 수와 현재 세션 수를 비교해본다. 여기서 같다고 하면, 현재 Session을 가져와서 For문을 돌려보면서 Session이 같은지 확인한다. 만약 기존에 등록된 Session과 현재 Session이 같다면 동일한 사용자의 로그인이기 때문에 문제가 없다.

    ConcurrentSessionControlAuthStrategy

    그렇지만 다른 사용자이기 때문에 결국 MaxSession을 초과했기 때문에 allowableSessionExceeded로 넘어오게 된다. 

    ConcurrentSessionControlAuthStrategy

    allowableSessionExceeded 메서드로 넘어오면, 현재 최대 동시 세션을 초과했을 때 어떤 정책을 사용하는지를 확인한다. 먼저 현재 인증 실패 전략을 체크하는데, 여기에 부합하게 되면 SessionAuthException을 발생시킨다. 이 때, SecurityContextHolder에서 Context를 Clear하고, Failure Handler에서 최종 실패 처리를 해준다. 

    만약 이전 세션 만료 전략일 경우, session들을 정렬한다. 그리고 가장 먼저 만들어진 세션부터 expireNow()를 통해 만료시킨다.

    session.expireNow()

    이 때, session은 SessionInformation이다. 즉, 진짜 Session을 Expire 하는 것은 아니고 SessionInfo 객체의 Expire 변수를 True로 바꿔준다. 실제 Session은 여기에서 만료가 되는 것이 아니다. 논리적으로 만료 처리를 해주는 것으로 이해할 수 있다. 

    ConcurrentSessionFilter

    사용자 1이 다시 자원에 접근하면 ConcurrentFilter에서 검사를 한다. ConcurrentSessionFilter는 매 요청마다 검사를 한다.

    ConcurrentSessionFilter

    이 때, Session이 있는 경우 Session Information 객체를 가져온다. 그리고 그 객체가 Expired된 것인지 확인한다. 만약에 Expire 되었다면 doLogout()을 호출해준다.

    ConcurrentSessionFilter

    Filter는 doLogout 메서드에서 인증 객체를 가져오고, request, response, auth를 handler들에게 넘겨줘서 logout을 처리한다.

    CompositeLogoutHandler

    CompositeLogoutHandler는 logoutHanlder를 가지고 있는데 여러 형태의 Logout Handler를 가지고 있고, 모든 LogoutHandler에 대해서 For문을 돌면서 로그아웃 처리를 해준다.

    ConcurrentSessionFilter

    그리고 ConcurrentSessionFilter로 돌아와서 onExpiredSessionDetected()를 이용해 사용자에게 세션이 만료되었음을 알려준다. 

    사용자에게는 이렇게 세션 만료 화면이 랜더링 된다. 

     

    참고 : RegisterSessionAuthStrategy

    RegisterSessionAuthStrategy에서 인증 객체에 대한 Session ID를 저장한다고 했다. 앞에서 key가 User 객체가 되어 Map에 저장하는 것으로 이야기를 했는데, 정확하게 이야기 하면 Map의 Value에 SESSION ID를 저장하게 된다. Map의 Value는 ArraySet로 Collection 형태로 JSSESION ID를 저장한다.

    댓글

    Designed by JB FACTORY