본문 바로가기

프로그래밍/Spring Security

Spring Security 3.2.4 적용에서 겪은 세션 동시성 관련 로그인 버그와 해결책..

요즘 Spring Framework를 Java를 이용하는 환경 설정을 적용해보면서 기존에 내가 블로그에 올렸던 Spring Security 또한 Java로 환경설정 하는 식으로 적용해보고 있다. 이와 관련된 글도 블로그로 따로 올릴 예정이다. 그러나 아직은 정리를 좀더 해야 할 부분이 있어서 일단은 적용 과정에서 겪은 내용 중 하나를 써볼까 한다.

처음 블로그에 글을 썼을 당시의 Spring Security 버전이 3.2.4여서 일단 이것으로 Java Config도 적용해보기로 했다. Spring Security Reference 문서와 기타 문서들, 그리고 구글링을 통해 기존 DB로 인증관리 하는 식으로의 XML 설정들을 모두  Java로 바꾸는데 얼추 정리가 되었다. 정리가 되다보니 몇몇 기능을 확인하는 과정을 거치게 되었는데 그중 했던것이 바로 세션관리였다. 흔히 Spring Security에서 말하는 중복 로그인 방지 기능(사이트에 동시 로그인 하는 것을 제약을 걸어주는 기능)이 들어가 있는 바로 그 부분이었다. 기존 XML 설정 관련 글에서는 이 내용을 언급하질 않았었는데 그것은 그 기능이 필요없어서..라기보단 활용 빈도가 극히 낮을것이라는 생각이 컸다(관리자 사이트 개발하는 것이 아닌 한에는 사용자의 동시 접속에 제약을 거는 상황이 그리 없을것이라는 생각이 들었다) 그래서 Authentication(인증)과 Authorization(권한)에 중심을 두어 블로그에 글을 썼던 것이다. 어차피 Spring Security에서도 강조하는 부분이 인증과 권한이고 세션관리는 그에 따른 부가 기능 성격이 크기 때문인것도 있었다.

 

암튼 이 세션관리를 테스트 해 보면서 한가지 버그를 발견하게 되었다. 예전에 XML 설정때도 글로 작성은 해보진 않았지만 테스트는 해봤던것 같아서 그때는 별 생각없이 넘어갔던 부분이 버그로 발견되어서 당혹스러운 것도 있었다. 버그의 내용은 다음과 같은 것이다.

 

Spring Security에서 세션관리 라 함은 Spring Security 의 Authentication 과정에서 겪게되는 사용자 정보의 세션 보관에 대한 내용이 큰데 이걸 하다보니 로그인 한 사용자의 동시접속 제한을 구현도 할 수 있게 되었다. 그래서 세션관리라 하면 본래의 목적보다  로그인을 한 유저의 동시접속에 제한이 기능적으로 더 크게 부각이 되었다. Spring Security에서의 로그인 과정에서의 세션 처리라 우리가 흔히 하는 로그인 과정의 세션 처리(로그인 하면 사용자 정보를 세션에 보관하고 로그아웃하면 invalidateSession 하는 식의 처리)과 다름이 없다보니 오히려 동시 접속 제한 기능이 더 부각이 되었다고 생각한다.

 

암튼 이 동시접속을 테스트해볼려고 동시 접속자의 수를 1로 설정했다. 즉 어떤 사용자의 로그인 ID로 현재 로그인 된 상태면 그 로그인 ID가 로그아웃 되기 전까지는 그 로그인 ID로 사이트에 로그인 할 수 없는 것이다. 사용자의 로그인 ID로 로그인 한 상태에서 그 아이디를 이용해 다시 로그인을 시도할 경우 예외가 발생했다. 이 증상은 정상이다. 로그인이 이미 되어 있는 상태이기 때문에 같은 아이디로 로그인할려고 한거니까 예외를 던진것이다. 그런데 해당 아이디를 로그아웃을 하고 다시 그 아이디로 로그인을 할려고 하면 로그인이 안되는 것이었다. 어? 로그아웃을 했기 때문에 로그인이 되어야 하는데 로그인이 안되다니..

 

그래서 확인해보기로 했다. 먼저 세션이 invalidate가 되는지를 확인하기 위해 jsp 페이지에서 Session ID를 출력해서 이 부분을 검증하고 싶었다. 그래서 다음과 Spring Framework의 @ControllAdvice 어노테이션이 걸린 클래스에 @ModelAttribute 어노테이션이 붙은 메소드에서 Seesion ID를 구해서 jsp 페이지에서 이를 언제든 확인할 수 있게 했다

 

import javax.servlet.http.HttpServletRequest;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.web.bind.WebDataBinder;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.InitBinder;
import org.springframework.web.bind.annotation.ModelAttribute;

import com.terry.springconfig.vo.MemberInfoVO;

@ControllerAdvice
public class GlobalInitBinder {
	private Logger logger = LoggerFactory.getLogger(this.getClass());
	
	@InitBinder
	public void binder(WebDataBinder binder) {
		
	}
	
	@ModelAttribute("sessionId")
	public String getSessionId(HttpServletRequest request){
		String sessionId = request.getSession().getId();
		logger.debug("sessionId : " + sessionId);
		return sessionId;
	}
}

 

위와 같이 @ControllerAdvice 어노테이션이 붙은 클래스에 @ModelAttribute 어노테이션을 이용하여 모든 jsp 페이지에서 sessionId란 이름으로 현재 Session의 Session ID 값을 가져올수 있게 했다. jsp에서 출력해본 결과 jsp 쪽의 Request Session에는 아무 문제가 없었다. 로그아웃을 하면 로그인이 마쳐진 뒤의 Session ID 값이 아닌 새로운 값을 가져올 수 있었다. 이걸 통해 invalidate는 정상적으로 이루어지는 것을 알수 있었다.

 

그러면 남은 부분은 Spring Security가 로그인 한 세션부분을 관리하는 쪽에서 무슨 문제가 있는 것이 아닐까하는 생각이 들어서 Spring Security의 Log Level을 TRACE로 설정한뒤 따져보기 시작했다. 로그아웃을 한 뒤 로그인을 시도할 경우 나오는 로그들 중에서 다음의 문구가 있었다

 

DEBUG o.s.s.w.a.UsernamePasswordAuthenticationFilter - Authentication request failed: org.springframework.security.web.authentication.session.SessionAuthenticationException: Maximum sessions of 1 for this principal exceeded

 

이렇게 나오면 안되는 것이다 왜냐면 로그아웃을 했기 땜에 세션은 0개일텐데 왜 Maximum Session 수로 설정한 1을 넘는다고 하는 것인가? 일단 이 로그가 org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter 클래스에서 찍히기 때문에 UsernamePasswordAuthenticationFilter 클래스 소스에서 이 로그를 출력하는 부분을 찾아보았다. 그러나 UsernamePasswordAuthenticationFilter 클래스 소스에서는 이 로그를 출력하는 부분을 찾을수 없었다. 그래서 클래스 정의를 보니 이 클래스가 org.springframework.security.web.authentication.AbstractAuthenticationProcessingFilter를 상속받는 것을 알게 되어서 AbstractAuthenticationProcessingFilter 클래스 소스에서 로그를 출력하는 부분을 찾게 되었고 결국 다음의 메소드에서 출력하고 있는 것을 알게 되었다

 

protected void unsuccessfulAuthentication(HttpServletRequest request, HttpServletResponse response,
            AuthenticationException failed) throws IOException, ServletException {
        SecurityContextHolder.clearContext();

        if (logger.isDebugEnabled()) {
            logger.debug("Authentication request failed: " + failed.toString());
            logger.debug("Updated SecurityContextHolder to contain null Authentication");
            logger.debug("Delegating to authentication failure handler " + failureHandler);
        }

        rememberMeServices.loginFail(request, response);

        failureHandler.onAuthenticationFailure(request, response, failed);
}

 

이 unsuccessfulAuthentication 메소드를 어디서 호출하고 있는 것일까? 이 메소드가 호출되는 위치는 AbstractAuthenticationProcessingFilter의 doFilter 메소드에서 다음과 같이 호출되고 있었다. UsernamePasswordAuthenticationFilter 클래스에서 doFilter 메소드에 대한 재정의를 하지 않았기 때문에 Filter 인터페이스에서 제공하는 doFilter 메소드를 호출하게 되면 AbstractAuthenticationProcessingFilter 클래스에서 정의된 doFilter 메소드를 호출하게 되는 것이다

public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain)
            throws IOException, ServletException {

        HttpServletRequest request = (HttpServletRequest) req;
        HttpServletResponse response = (HttpServletResponse) res;

        if (!requiresAuthentication(request, response)) {
            chain.doFilter(request, response);

            return;
        }

        if (logger.isDebugEnabled()) {
            logger.debug("Request is to process authentication");
        }

        Authentication authResult;

        try {
            authResult = attemptAuthentication(request, response);
            if (authResult == null) {
                // return immediately as subclass has indicated that it hasn't completed authentication
                return;
            }
            sessionStrategy.onAuthentication(authResult, request, response);
        } catch(InternalAuthenticationServiceException failed) {
            logger.error("An internal error occurred while trying to authenticate the user.", failed);
            unsuccessfulAuthentication(request, response, failed);

            return;
        }
        catch (AuthenticationException failed) {
            // Authentication failed
            unsuccessfulAuthentication(request, response, failed);

            return;
        }

        // Authentication success
        if (continueChainBeforeSuccessfulAuthentication) {
            chain.doFilter(request, response);
        }

        successfulAuthentication(request, response, chain, authResult);
}

 

이 doFilter 메소드를 보면 로그를 출력하는 메소드인 unsuccessfulAuthentication 메소드를 예외가 발생하게 되는 경우 try-catch의 catch 부분에서 호출하고 있다. 그래서 try 안에 중단점을 걸고 어떤 예외가 발생되어서 catch 부분의 unsuccessfulAuthentication 메소드를 호출하게 되는지 알아보면 되는 것이다. Eclipse에서 try 안에 있는

 

authResult = attemptAuthentication(request, response);

 

이 부분에 중단점을 걸은 뒤 디버그 모드로 서버를 구동시켜 추적을 해보자. 이렇게 추적을 해보면 중단점을 걸은 부분은 예외가 발생하지 않았다. 다음 줄인 authResult가 null인지를 체크하는 부분도 잘 통과가 되었다. authResult가 null이 아닌 특정 객체로 return을 받았기 때문이다. 이 글을 읽고 있는 독자분들께 부탁의 말이 있다면 authResult = attemptAuthentication(request, response); 이 부분을 Step Into로 계속 파고들어가면서 디버깅 해보기 바란다. Step Into로 파고 들면서 디버깅 해보면 authResult가 무엇을 return 하게 되는지 알기 때문이다. 미리 말씀 드리면 authResult가 리턴받은 값은 로그인 한 사용자의 인증정보를 받게 된다. 즉 if문이 하는 역할은 인증정보가 없어서 null을 받게 되면 다음단계를 진행하지 않기 위해 그냥 return 하게 되는 것이다. 그럼 if 문 다음에 있는 줄인 sessionStrategy.onAuthentication(authResult, request, response); 이 부분에서 예외가 발생하게 되는 것일까? Step Into로 들어가서 확인을 해보자.

 

변수 sessionStrategy SessionAuthenticationStrategy 인터페이스 타입으로 AbstractAuthenticationProcessingFilter 클래스의 멤버변수로 정의되어 있으며 멤버변수로 정의되는 시점에 다음과 같이 NullAuthenticatedSessionStrategy 클래스 객체로 초기화가 된다.

 

private SessionAuthenticationStrategy sessionStrategy = new NullAuthenticatedSessionStrategy();

 

그러나 sessionStrategy에 대한 setter 메소드가 존재하기 때문에 SessionAuthenticationStrategy 인터페이스를 구현한 클래스로 다시 재정의가 될 여지가 있기 때문에 이클립스의 디버그모드에서 이 변수가 어떤 타입의 객체를 받았는지 확인해볼 필요가 있다. 실제로 확인해보면 sessionStrategy 변수에 할당된 객체는 org.springframework.security.web.authentication.session.CompositeSessionAuthenticationStrategy 클래스 객체가 된다. Step Info로 한번 파고 들어가보자.

 

CompositeSessionAuthenticationStrategy 클래스의 onAuthentication 메소드는 다음과 같이 정의되어 있다.

 

public void onAuthentication(Authentication authentication,  HttpServletRequest request, HttpServletResponse response)  throws SessionAuthenticationException {
        for(SessionAuthenticationStrategy delegate : delegateStrategies) {
            if(logger.isDebugEnabled()) {
                logger.debug("Delegating to " + delegate);
            }
            delegate.onAuthentication(authentication, request, response);
        }
}

 

코드를 보면 delegateStrategies 변수의 값들을 하나하나  꺼내서 onAuthentication 과정을 거치고 있다. 그러면 delegateStrategies 변수가 어떤건지 확인해봐야겠다. 이 변수는 CompositeSessionAuthenticationStrategy 클래스에서 다음과 같이 멤버변수로 정의되어 있다.

 

private final List<SessionAuthenticationStrategy> delegateStrategies;

변수를 선언한 시점에 초기화를 하지 않았고 또한 final 연산자가 붙어있기 때문에 이 변수를 초기화 할 수 있는 시점은 생성자외에는 없다. 그래서 이 클래스의 생성자를 보면List<SessionAuthenticationStrategy> 객체를 파라미터로 받아 이 멤버변수를 초기화하는 것을 알 수 있다. 이 클래스의 역할을 짐작해보면 여러개의 SessionAuthenticationStrategy 인터페이스를 구현한 클래스들을 이용해서 인증 작업을 시도할려고 할 경우 이렇게 CompositeSessionAuthenticationStrategy 클래스에 해당 클래스들을 넣은 List 객체를 생성자로 넣어서 인증과정을 하게끔 만든것으로 생각해볼수가 있다. 암튼 이 부분을 파보면 delegateStrategies 변수에 들어있는 객체중 첫번째에 들어있는 org.springframework.security.web.authentication.session.ConcurrentSessionControlAuthenticationStrategy 객체의 onAuthentication 매소드를 실행할려고 하게 된다. 그럼 이 클래스의 onAuthentication 메소드를 살펴보자. 이 메소드의 소스는 다음과 같다

 

    public void onAuthentication(Authentication authentication, HttpServletRequest request,
            HttpServletResponse response) {

        final List<SessionInformation> sessions = sessionRegistry.getAllSessions(authentication.getPrincipal(), false);

        int sessionCount = sessions.size();
        int allowedSessions = getMaximumSessionsForThisUser(authentication);

        if (sessionCount < allowedSessions) {
            // They haven't got too many login sessions running at present
            return;
        }

        if (allowedSessions == -1) {
            // We permit unlimited logins
            return;
        }

        if (sessionCount == allowedSessions) {
            HttpSession session = request.getSession(false);

            if (session != null) {
                // Only permit it though if this request is associated with one of the already registered sessions
                for (SessionInformation si : sessions) {
                    if (si.getSessionId().equals(session.getId())) {
                        return;
                    }
                }
            }
            // If the session is null, a new one will be created by the parent class, exceeding the allowed number
        }

        allowableSessionsExceeded(sessions, allowedSessions, sessionRegistry);
}

 

이 메소드에서 파라미터로 넘기고 있는것은 우리가 아이디와 패스워드를 입력하여 받게 되는 Spring Security가 만들어주는 Authentication(인증) 정보들이 들어있는 객체와 HttpServletRequest 객체와 HttpServletResponse 객체이다.이 코드의 첫번째 줄에 있는 sessionRegistry 변수는 ConcurrentSessionControlAuthenticationStrategy 클래스의 멤버변수로 다음과 같이 정의되어 있다

 

private final SessionRegistry sessionRegistry;

 

이 멤버변수 또한 final로 선언되어 있어서 생성자를 통해 값을 받고 있다. 이 변수는 SessionRegistry 인터페이스를 구현한 클래스 객체가 오게 되는데 실제 오게 되는 클래스 객체는 org.springframework.security.core.session.SessionRegistryImpl 클래스 객체가 오게 된다. 이 SessionRegistryImpl 클래스가 하게 되는 역할을 먼저 알아둘 필요가 있는데 이 객체는 사용자의 인증정보와 그 인증정보가 가지고 있는 세션들(한 사람의 사용자가 여러 세션을 생성하면서 들어올수도 있다. 브라우저를 서로 달리한뒤 각각 로그인 하게 되면 하나의 사용자가 2개의 서로 다른 세션 ID를 갖게 되는 이치이다)의 Session ID 값들을 Map 객체에 넣어서 보관하게 된다. 정확하게 얘기하면 Spring Security에서 사용하는 사용자 정보 객체인 Principal 객체를 Key로 하고 해당 사용자가 가지고 있는 Session ID 값들이 들어가 있는 Set 객체를 Value로 하는 ConcurrentMap에 보관하게 된다. 이런 구조로 보관함으로써 해당 사용자가 가지고 있는 Session ID 값들을 알수 있게 되는 것이다. 실제로 이 내용을 저장하도록 SessionRegistryImpl 클래스는 다음의 멤버변수들을 가지고 있다.

 

private final ConcurrentMap<Object,Set<String>> principals = new ConcurrentHashMap<Object,Set<String>>();
private final Map<String, SessionInformation> sessionIds = new ConcurrentHashMap<String, SessionInformation>();

 

이것들도 final로 선언되어 있지만 선언시에 초기화를 시켰기 때문에 위에서 언급했던 final 변수들과는 달리 외부에서 생성자를 통해 값을 주입하는게 아닌 SessionRegistryImpl 클래스의 메소드들이 이 변수의 내부 값들을 변경하게 된다. principals 변수에 들어가는 형태는 사용자 정보를 의미하는 Pricipal 인터페이스를 구현항 객체를 key로 하고 이 사용자 정보와 연관된 Session ID 값들이 들어있는 Set 객체를 Value로 하는 Map이다. sessionIds 변수에 들어가는 형태는 Session ID를 key로 하고 해당 Session에 대한 정보를 가지고 있는 SessionInformation 클래스 객체를 Value로 하는 Map이다, 암튼 다시 원점으로 돌아와서 checkAuthenticationAllowed 메소드를 분석해보자. 첫번째로 다음과 같은 코드가 있다

 

final List<SessionInformation> sessions = sessionRegistry.getAllSessions(authentication.getPrincipal(), false);

 

코드의 메소드명을 가지고 추측해보면 이 코드가 하는 일은 인증정보에서 사용자 정보를 빼내온뒤 이를 파라미터로 넘겨서 사용자가 갖고 있는 모든 세션 정보를 넘겨받는 느낌을 준다. 실제로 그러한지 SessionRegistryImpl 클래스의 getAllSessions 메소드를 보도록 하자

 

public List<SessionInformation> getAllSessions(Object principal, boolean includeExpiredSessions) {
        final Set<String> sessionsUsedByPrincipal = principals.get(principal);

        if (sessionsUsedByPrincipal == null) {
            return Collections.emptyList();
        }

        List<SessionInformation> list = new ArrayList<SessionInformation>(sessionsUsedByPrincipal.size());

        for (String sessionId : sessionsUsedByPrincipal) {
            SessionInformation sessionInformation = getSessionInformation(sessionId);

            if (sessionInformation == null) {
                continue;
            }

            if (includeExpiredSessions || !sessionInformation.isExpired()) {
                list.add(sessionInformation);
            }
        }

        return list;
 }

 

예상대로 해당 사용자가 가지고 있는 모든 세션 정보(SessionInfomation)을 return 하게 해준다. getSessionInformation 메소드에 특정 session id 값을 parameter로 넘겨주어 그에 대한 세션 정보(SessionInformation) 객체를 받아오고 있다. 여기서 한가지 알아두어야 할 것이 있다. parameter로 받은 includeExpiredSessions이다. 사용자가 여러 세션을 가질수 있다고는 위에서 언급했었다. 그러다보니 그런 세션들중 만기가 된 세션들도 분명 있을것이다. 그러면 Spring Security는 만기가 된 세션의 정보를 없애는 것이 아니라 이 세션은 만기가 되었다고 표시를 세션 정보(SessionInfomation)에 기록해둔다. 실제로 org.springframework.security.core.session.SessionInformation 클래스의 소스를 보면 이를 보관하는 boolean 타입의 expired란 멤버변수가 있다. 암튼 getAllSessions 메소드를 호출할때 includeExpiredSessions를 false로 했으므로 만기되지 않은 세션정보들만 넘어오게 된다.

 

이제 checkAuthenticationAllowed 메소드의 다음줄을 보자. 다음줄은 다음과 같이 되어 있다

 

int sessionCount = sessions.size();

 

단순하게 보면 위에서 구한 만료되지 않은 세션 정보들의 갯수들을 가져오고 있다. 근데 여기에서 버그가 시작됨을 알수있게 된다. 위에서 언급했던 테스트 상황을 생각해보자. 로그인을 한 뒤에 로그아웃을 하고 다시 로그인을 하는 과정에서 우리는 위의 코드들을 디버깅하고 있는 중이다. 그러면 sessionCount는 얼마가 들어있어야 하는가? 로그아웃을 했기 때문에 만료되었을 것이고 만료된 세션은 List 객체에 넣지 않기 때문에 0이어야 한다. 근데 이 코드를 실행해보면 sessionCount는 1을 가지고 있음을 디버깅 하면서 알게 된다. 어? 왜 1을 가지지? 만료된 세션은 List 객체에 넣지 않는다고 했는데..이제 sessions 변수 안을 파고 들어가보자. 이 변수를 파고 들어가보면 예전 로그인을 한 뒤의 Session ID 값을 가진 SessionInformation 객체가 들어있다. 근데 문제는 이 객체의 만료여부를 기록하는 멤버변수인 expired 멤버변수가 false로 되어 있다는 것이다. 로그아웃을 거쳤기 때문에 expired 멤버변수는 true로 셋팅되어 있어야 하는데 맞는데도 로그아웃을 거친뒤에도 expired 변수값을 true로 셋팅하지 않고 있는 것이다. 이 부분때문에 checkAuthenticationAllowed 메소드가 전반적으로 오동작을 일으켜서 로그인을 한뒤 로그아웃을 하고 다시 로그인을 하는데도 로그인이 안되는 증상이 생기고 있는것이다.

 

자 다시 정리해보자. 로그인을 하기전엔 A라는 세션 ID를 가지고 있을 것이다. 사용자 U로 로그인을 거치게 되면 세션 ID를 B로 재할당을 받게 되고 세션 ID B는 SessionInformation 클래스 객체로 만들어지게 되며 사용자 U는 방금 만든 B SessionInformation 클래스 객체를 갖는 형태로 Spring Security에서 관리가 된다. 이제 사용자 U를 로그아웃을 하게 되면 세션 ID B를 저장하고 있는 SessionInformation 클래스 객체의 expired 변수값을 true로 해서 저장을 한뒤 세션이 invalidate 되기 때문에 세션 ID C를 할당받게 된다. 로그인 한뒤 로그아웃을 했기땜에 사용자 U로 로그인 된 세션은 없다. 이 상태에서 다시 로그인을 시도하게 되면 Spring Security가 사용자 U와 연관된 SessionInformation 객체의 세션 ID B는 expired 되어 있는 상태이기 때문에 Spring Security 설정시 동시 접속자수를 1로 설정되어 있는 경우라면 로그인이 되어야 하는 것이 맞다. 그러나 세션 ID B에 대한 SessionInformation 객체의 expired 멤버변수가 계속 false로 유지되고 있기 때문에 아직 접속중이라고 판단하고 로그인을 할 수 없다는 식의 예외 동작을 하게 되는 것이다.

 

장황하게 설명이 길었는데, 이 부분이 이해가 안된다면 Eclipse에서 ConcurrentSessionControlAuthenticationStrategy 클래스의 onAuthentication 메소드가 정의된 부분의 첫번째 줄에 중단점을 걸고 디버깅을 하면서 만나게 되는 각 값들의 내부 상태값을 일일이 확인하면서 이 글을 살펴보길 바란다. 실제로 이 글을 쓰는 과정에서도 이 방법을 사용하여 설명했다. 그럼 이 버그를 어떻게 해결해야 하는가? expired 변수를 true/false로 설정하는 것은 외부에서 설정하는게 아니라 Spring Security가 하기 때문에 우리가 인위적으로 설정할수 있는 부분이 없다. 그리고 이 블로그에서는 Spring Security 3.2.4를 사용하고 있으나 이 문제는 Spring Security 3.2.3에서도 발생하고 있는 문제였다. 구글링을 하면서 나와 같은 상황을 접한 사용자가 올린 질문을 찾게 되었다.

 

http://stackoverflow.com/questions/23455175/spring-security-3-2-and-maximumsessions-logging-out-not-updating-sessionregist

 

지금 보는 글에서는 언급되지 않은 부분이 있으나 위의 링크 글에는 언급된 부분이 있는데, 로그인을 하게 되면 SessionRegistryImpl 클래스의 registerNewSession 메소드를 실행하여 위에서 언급했던 멤버변수인 sessionIds 멤버변수에 세션 정보를 넣고 있다. 이런 구조라면 로그아웃을 하는 시점엔 sessionIds 멤버변수에 넣은 세션정보를 지우거나 또는 expired 멤버변수를 true로 설정하는 작업을 해주는 부분이 있어야 하는데 그런 작업을 하지 않고 있다. 이 부분에 대해서는 로그아웃을 하는 부분에 대해 디버깅을 걸고 추적해보면 알수 있다. 그래서 동시성 제어에 대한 버그가 존재하고 있는 것이다.

 

마지막으로 해결책을 얘기하고 이 글을 마치고자 한다. 하루를 끙끙대보았으나 의외로 해결책은 간단했다. Spring Security를 버전업 해주면 된다. 현재 Spring Security 3.2.X 계열 버전으로 가장 최신 버전은 Spring Security 3.2.7 버전인데 이 버전으로 업그레이드를 하니 위와 같은 동시성 제어 버그가 해결이 되었다. 각 버전이 업그레이드 될때마다 버그 fix 관련 내용을 정리를 해서 내보내게 되는데 아직 이 부분에 대해 언급한 내용을 찾질 못했다. 3.2.5, 3.2.6, 3.2.7 중 한군데서 해결된거 같은데 어느 버전인지는 모르겠다. 머 모든 버전에서 테스트해보면 알수 있겠지만..굳이 그래야 할까 싶다. 하지만 그래도 변경된 점을 한번 체크해보기로 했다(갠적으로 궁금해서..)

 

일단 로그아웃을 하게 되면 로그아웃 당시의 세션값도 보관하는 식이었으나 지금은 보관하지 않게끔 바뀌어서 하나의 브라우저로 테스트를 진행할 경우 principals에는 아무 값도 들어있지 않게 된다. 그래서 나중에 getAllSessions 메소드 실행시 비어있는 List 객체를 return 하기 때문에 전체적으로 정상동작이 이루어지게 되는 것이었다. 정리하자면 내부 동작 방식에 수정을 가해서(expired 된 세션 정보에 대한 보관을 하지 않는 식으로..) 정상 동작이 되도록 했다.