• 카테고리

    질문 & 답변
  • 세부 분야

    백엔드

  • 해결 여부

    미해결

동시 세션 제어 - 동일 브라우저에서 로그아웃이 정책 미적용

20.07.02 11:09 작성 조회수 3.69k

2

안녕하세요.

강좌를 유익하게 잘 보고 있습니다.

동시 세션 제어 부분에서 해당 Security 2.3.1 버전을 사용하고 있는데 

"maxSessionsPreventsLogin(true)"

상태에서 아래와 같은 동작 시  "Maximum sessions of 1 for this principal exceeded" 오류 발생으로 제대로 동작을 하지 않고 있습니다. 

login -->  logout -->  재 login

해당 이슈를 검색을 해보니 

"https://stackoverflow.com/questions/41429778/spring-security-logout-and-maximum-sessions"

에서 해결점을 찾을 수 있었지만. securityRegistry 가 Bean으로 노출되지 않는 상태라서 그렇다는데.. 이부분이 이해가 가지 않아서요

버그 인지 실제 스프링 정책인 것인지 , 아니면 스프링에서 유도(?) 하는 코딩  방식이 있는 것인지 궁금합니다.

행복한 날 되세요 

답변 1

답변을 작성해보세요.

8

네 조금 어려운 주제이긴 합니다만 최대한 쉽게 풀이해 보도록 하겠습니다.

다만 스프링 시큐리티에서 login > logout > login 시

logout 실행만을 통해서 동시적 세션 제어가 작동하도록 하지 않고 SessionRegistry 와 HttpSessionEventPublisher 를 통해서 작동하도록 설계한 부분은 동시적 세션제어의 핵심적인 기능을 구현한 클래스들이 LogoutFilter 와 상호간 호출을 통해서 실행하고 작동하기가 어려운 구조로 되어 있기 때문입니다

일단 기억할점은 동시적 세션 제어 최대 허용 개수의  기준은 현재 로그인한 사용자의 sessionId 를 키로 하는 사용자의 세션 정보 객체(SessionInformation) 의 개수를 제어하는 구조입니다.

순서로 보면 다음과 같습니다.

1. 처음 로그인 시 SessionRegistryImpl 클래스가 사용자 세션 정보를 (SessionInformation)를 Set 에 저장함

    - 동시적 세션 제어는 인증 시 Set 에 저장된 사용자 정보를 담고 있는 객체(SessionInformation)의 카운트를 기준으로 세션제어 최대 허용 개수와 비교함

// 인증 시 sessionId 를 키로 해서 사용자의 정보를 SessionInformation 에 저장하고
// 만약 동일한 계정으로 로그인했지만 sessionId 가 틀린 경우는 두개의 SessionInformation 이 생성된다
// 즉 동일한 계정으로 로그인한 현재 세션이 2개가 된다

sessionIds
.put(sessionId,
new SessionInformation(principal, sessionId, new Date()));

2. 로그 아웃 실행하면 사용자의 세션이 무효화 되긴 하지만 해당 사용자의 SessionInformation 는 여전히 Set 에  존재함

   - LogouFilter 에서 세션을 무효화 하더라도 Set 에 저장된 SessionInformation 까지 삭제하지 않음. 즉, Logout 을 실행하는 더라도 동시적 세션 처리까지 다 해 주는것은 아님

// 아래 구문에서 동시적 세션 처리를 위한 작업을 하지 않고 세션 무효화 등의 일을 처리하고 있음
// 로그아웃시 호출되는 이벤트 리스너에서 동시적 세션을 처리하도록 함(HttpSessionEventPublisher)

this
.handler.logout(request, response, auth); // 세션 무효화 등..
logoutSuccessHandler.onLogoutSuccess(request, response, auth); // 로그인 페이지로 리다이렉트 등..

3. 로그아웃 후 동일 브라우저에서 로그인을 다시 실행하는 과정에서 동시적 세션 검증을 위해 Set 에서 정보를 조회하니 현재 로그인을 시도하는 사용자 객체 (principal)을 키로 하는 SessionInformation 이 Set 에 이미 저장되어 있음을 확인

// 동일 계정의 principal 로 저장된 Set 정보 가지고 옴
final
Set<String> sessionsUsedByPrincipal = principals.get(principal);

.. 중략

// sessionId 에 해당하는 SessionInformation 정보 조회하고 존재하는지 확인
for (String sessionId : sessionsUsedByPrincipal) {
SessionInformation sessionInformation = getSessionInformation(sessionId);

if (sessionInformation == null) {
continue;
}

// 세션이 만료된 정보는 카운트 하지 않는다 // includeExpiredSessions 은 기본값으로 false 이고 sessionInformation 의 expired 업데이트는 // maxSessionsPreventsLogin(false) 일 경우 별도로 구현되어 있기 때문에 // maxSessionsPreventsLogin(true) 인 경우는 아래 조건에 부합하지 않음 // 그래서 Logout 되더라도 sessionInformation 의 expried 속성은 여전히 false 상태임 // 서두에서 언급한 것 처럼 LogoutFilter 에서 sessionInformation 의 expired 속성을 // true 로 하면 될 것 같은데 구조상 LogoutFiler 에서 sessionInformation 의 정보를 참조하거나 // 관련된 메서드를 호출하는 것은 좋은 설계가 아니라는 판단이 듦 // 다만 어떠한 형태로든 로그아웃 시점에서 동시적 세션처리 관련 작업이 이루어져야 하는 것은 분명하므로 // 로그아웃 시 발생하는 이벤트 리스너를 사용하여 해결하고 있는 것으로 판단됨


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

4. 3번의 결과에서 나온  사용자의 sessionId 와 현재 로그인 시도하는 사용자의 sessionId 를 비교해서 같은 값인 경우 세션 최대 허용을 벗어나지 않으며 서로 다른 값인 경우 비록 동일 사용자라 할지라도 서로 다른 세션이라고 판단해서 "Maximum sessions of 1 for this principal exceeded"  발생시킴. 


// 현재 동일계정의 세션개수와 최대 허용된 세션개수가 같을 경우
if
(sessionCount == allowedSessions) {

HttpSession session = request.getSession(false);

if (session != null) {

// 현재 사용자의 세션 ID 와 Set 에 저장되어 있는 SessionInfomation 의 세션 ID 를 비교하여
// 동일한 값이면 세션 허용 최대 개수를 초과하지 않음으로 판단

for (SessionInformation si : sessions) {
if (si.getSessionId().equals(session.getId())) {
return;
}
}
}

}

// 세션 허용 개수를 초과하여 아래 구문을 호출함
allowableSessionsExceeded(sessions
, allowedSessions, sessionRegistry);

protected void allowableSessionsExceeded(List<SessionInformation> sessions,
int allowableSessions, SessionRegistry registry)
throws SessionAuthenticationException {
if (exceptionIfMaximumExceeded || (sessions == null)) {
throw new SessionAuthenticationException(messages.getMessage(
"ConcurrentSessionControlAuthenticationStrategy.exceededAllowed",
new Object[] {allowableSessions},
"Maximum sessions of {0} for this principal exceeded"));
}

}

< 해결 방안>

1. 로그아웃 시 SessionRegistryImpl 와 HttpSessionEventPublisher 를 통해 문제 해결

   - 세션이 생성되거나 페기될 때 호출되는 HttpSessionEventPublisher 클래스를 리스너로 등록함

@Bean
public static ServletListenerRegistrationBean httpSessionEventPublisher() {
return new ServletListenerRegistrationBean(new HttpSessionEventPublisher());
}

   - 로그아웃시 HttpSessionEventPublisher 클래스의 sessionDestroyed(HttpSessionEvent event) 메서드가 호출되고 ApplicationContext.publishEvent(HttpSessionDestroyedEvent) 를 실행하여 HttpSessionDestroyedEvent 를 발생시킴

public void sessionDestroyed(HttpSessionEvent event) {
HttpSessionDestroyedEvent e = new HttpSessionDestroyedEvent(event.getSession());
Log log = LogFactory.getLog(LOGGER_NAME);

if (log.isDebugEnabled()) {
log.debug("Publishing event: " + e);
}

getContext(event.getSession().getServletContext()).publishEvent(e);
}

   - SessionRegistryImpl 는 ApplicationListener<SessionDestroyedEvent> 인터페이스를 구현했기 때문에 위에서 publishEvent(HttpSessionDestroyedEvent) 가 실행되면 onApplicationEvent(SessionDestroyedEvent event) 가 호출되고 여기에서 removeSessionInformation(sessionId) 구문을 실행하는데 이 구문이 Set 에 저장된 SessionInformation 를 삭제함.

public void onApplicationEvent(SessionDestroyedEvent event) {
String sessionId = event.getId();
removeSessionInformation(sessionId);
}

2. 로그아웃 후 다시 로그인을 시도하더라도 이미 로그인한 동일한 계정의 SessionInformation 이 1번의 과정을 통해 삭제되었기 때문에 SessionInformation 의 count 가 0 으로 줄어든 상태이고 최대 세션 허용 개수를 벗어나지 않음

전체 내용이 제법 많고  텍스트로 설명 드리니까 잘 이해가 안가실 수 있습니다.

핵심은 동시적 세션제어는 인증 사용자의 SessionInformation 의 개수를 가지고 최대 세션 허용 개수의 초과 여부를 판단하는 것이고 로그아웃 시 동시적 세션 제어가 가능하도록 SessionInformation 정보까지 삭제하는 기능을 가진 클래스가 SessionRegistryImpl 와 HttpSessionEventPublisher 라고 보시면 됩니다.

SessionRegistryImpl 와 HttpSessionEventPublisher 를 찬찬히 디버깅하시면서 분석해 보시면 동시적 세션 제어를 가능하게 하는 기능들이 구현되어 있음을 알게 됩니다.

그리고 SecuritConfig 클래스를 여러개 만들어서 운용하는 다중 보안 설정이 아니라면 SessionRegistryImpl 이 반드시 Bean 일 필요는 없습니다.

어차피 Bean 이 아니더라도 SecurityConfig 가 한개라면 SessionRegistryImpl 는 각 인증 사용자에 대한 SessionInformation 의 카운트 참조에 동기화를 보장합니다.

다만 DI 를 해서 다른곳에서 사용할 필요가 있다면 Bean 으로 설정하시면 됩니다.

요약하면 HttpSessionEventPublisher 를 설정클래스에서 리스너로 등록하시면 로그아웃시 세션 삭제 이벤트를 받게 되고 다시 SessionRegistryImpl 에서 이벤트를 받아 세션 사용자 정보를 삭제해서 세션 최대 허용 개수를 관리하는 등의 동시적 세션 제어가 가능하도록 한다는 점입니다.

내용이 다소 길어졌는데 위 내용을 중심으로 소스를 찬찬히 들여다 보시기 바랍니다.