Java

[Spring] 동시 접속 차단 제한 (중복 로그인에 대한 처리) 방법

 

스프링 시큐리티

한 때 개발자가 직접 처리하던 보안 관련 코딩(시큐어 코딩) 처리 과정을 스프링 프레임워크에서 제공하는 스프링 시큐리티를 사용하여 사용권한 관리, 비밀번호 암호화, 회원가입, 로그인, 로그아웃 등의 웹 보안 관련 기능 개발을 쉽게 처리할 수 있다. 스프링 시큐리티의 강점과 단점 도 있다.

 

동시 접속 차단

하나의 계정으로 동시접속을 방지하기 위해서는 자바 스프링 시큐리티(Spring Security) context-security.xml 설정 파일에 다음과 같이 session-management엘리먼트를 사용하여 설정을 추가해주면된다.

최대 세션수 max-sessions 를 지정한다.
무제한 접근 허용이 필요하다면 -1을 값으로 설정하면 된다.

이렇게 처리해주면 로그인을 한 후 다시 로그인을 시도하는 경우

첫번째 로그인한 것은 세션이 초기화(무효화) 되고 두번째 로그인에 대해서 세션이 연결된다.

	<s:http use-expressions="true">
		...생략...
		<s:session-management>
	       	<s:concurrency-control max-sessions="1" expired-url="/loginDuplicate.do"/>
	   	</s:session-management>		
	</s:http>

세션 만료시 이동할 주소 설정은 expired-url 속성값을 사용하여 처리한다.

두번째 로그인을 차단하고 싶다면 error-if-maximum-exceeded=”true” 로 설정해주면 

처음 로그인 정보를 유지할 수 있다. 

<concurrency-control max-sessions="1" error-if-maximum-exceeded="true" />

 

만약 아래와 같이 로그인폼( form-login )을 사용하고 있을 경우

<s:form-login login-page="/login.do" 
	login-processing-url="/loginCheck.do"
authentication-failure-handler-ref="SimpleUrlAuthenticationFailureHandler를 상속받아 만든 class" 
authentication-success-handler-ref="AuthenticationSuccessHandler를 상속받아 만든 class"/>

 

로그인실패시 authentication-failure-handler-ref=”” 설정한 클래스를 호출하게 된다.

에러페이지를 설정하고자 하는 경우에는  session-management 요소에 session-authentication-error-url 속성을 추가하면 된다.

 

동시접근을 제어하기 위해 스프링 시큐리티는 다음과 같이 간단하게 추가할 수 있도록 지원한다.

세션 생명주기와 관련된 이벤트를 스프링 시큐리티가 받을 수 있도록 하기 위해, web.xml 파일에 추가할 필요가 있다.

 

세션을 만료 처리 하였음에도 제거되지않는 문제가 발생하는 경우, web.xml 파일에 HttpSessionEventPublisher를 등록해준다. 이 문제는 error-if-maximum-exceeded=”true” 를 사용하는 경우, 이전 로그인을 끊고 다시 로그인해도 접근되지않는 문제가 발생하게 되었다.

<!-- Session Create/Destroy Event Publisher -->
<listener>
	<listener-class>org.springframework.security.web.session.HttpSessionEventPublisher</listener-class>
</listener>

또한  error-if-maximum-exceeded=”true” 를 사용하는 경우, 세션 만료로 기존 로그인이 끊어진 경우

로그인 시도시 정상적으로 접근이 됨을 확인하였다.

 

 

스프링 시큐리티 사용시 로그아웃 혹은 세션시간 만료시점에 따른 DB에 저장한 세션 삭제처리 또는 로그아웃 시간 체크하기 위해

세션 만료 이벤트를 잡아서 처리하기 위해  ApplicationListener<SessionDestroyedEvent>를 구현하여 처리한다.

그리고 난 후 스피링 빈으로 등록해주어야한다.  context-security.xml에 추가하면 잘 호출 된다.

import java.util.List;
import javax.annotation.Resource;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationListener;
import org.springframework.security.core.context.SecurityContext;
import org.springframework.security.core.session.SessionDestroyedEvent;

public class SessionDestroyedListener implements ApplicationListener<SessionDestroyedEvent> {

	@Override
	public void onApplicationEvent(SessionDestroyedEvent event) {
		List<SecurityContext> contexts = event.getSecurityContexts();

        if (!contexts.isEmpty()) {
            for (SecurityContext ctx : contexts) {
                // 로그아웃 된 유저정보
                User cu = (User) ctx.getAuthentication().getPrincipal();                 

                // 로그아웃  DB delete처리    
                
//                Authentication authentication = securityContext.getAuthentication(); 
//                String userName = authentication.getName();
               

            }
        }
		
	}

}

 

그렇다면, 

첫번째 로그인 사용자를 유지한 후에

다른 브라우저에서 혹은 다른 pc에서 로그인했을 때 

사용자에게 “이미 로그인되어있음, 기존 연결을 끊고 다시 로그인할래요? 라는 메세지를 던진 후

사용자가 “예”를 선택했을 때 , 이전 로그인자의 세션id (로그인시 세션ID를 DB처리 해둔 상태 일때)는
DB에서 가져오면 되는데 세션 만료 처리는 어떻게 해야할까?

어떻게 하면 HttpSession session를 이전 사용자 세션 정보를 생성 후 
session.invalidate(); 처리를 할 수 있을까?

어떻게 하면 SessionRegistry에서 expired시킬 수 있는가?
sessionRegistry의 removeSessionInformation메소드로는 만료되지 않았다. 

@Autowired
SessionRegistry sessionRegistry;
.....생략

sessionRegistry.getAllPrincipals();    	
sessionRegistry.removeSessionInformation(sessionId);

 

 

스프링 시큐리티 동시접속자 이전 세션 만료 처리하는 방법

SessionInformation 클래스를 사용하여 세션에 대한 만료 여부를 체크 할 수 있고, 만료되지 않았다면 exipreNow()를 호출해주면 처리된다.

String sessionId = 'DB에서 조회한 sessioniD';

SessionInformation sessionInformation = sessionRegistry.getSessionInformation(sessionId);
if(!sessionInformation.isExpired()) {
    sessionInformation.expireNow();
}

 

 

 

전자정부프레임워크 포털 사이트에서 묻고 답하기 탭을 이용하여 “로그아웃”으로 검색시 많은 질의답변결과를 참고하면 도움이 될 수 있다.

 

묻고 답하기 | 표준프레임워크 포털 eGovFrame

처리중입니다. 잠시만 기다려주십시오.

www.egovframe.go.kr

 

참고로, 브라우저를 닫거나 alt+F4와 같은 방식으로 종료하는 경우 현재 HTTP로 서비스되는 처리 상 connectionless 방식이기 때문에 세션 만료 처리는 불가능하다.

 

 

스프링 시큐리티를 사용하지 않고 중복로그인을 해결하는 방법

 

egovframework:com:v3:cmm:multilogin [eGovFrame]

spring security 없이 중복 로그인을 방지하기 위한 기능이다. 로그인할 때 로그인 ID와 세션 ID를 Map에 저장하고, 로그아웃할 때 혹은 세션타임아웃 설정에 따라 두 정보를 Map에서 제거한다. 이미 로

www.egovframe.go.kr

 

HttpSessionListener

HttpSessionListener를 통해 세션을 관리할 수 있는데,

해당 객체는 Session이 생성되거나 제거될때 발생하는 이벤트를 제공한다.

sessionDestroved()는 세션이 invaild 되는 시점에 호출되며, setMaxInactiveInterval 에 지정해 놓은 세션 타임아웃이 적용되면 동일하게 호출된다. 스프링 시큐리티를 사용하고 있지않다면 사용하는게 좋을 듯 하다.

// @WebListener
public class CustomHttpSessionListener implements HttpSessionListener {
	private static final Map<String, HttpSession> sessions = new ConcurrentHashMap<>();

	@Override
	public void sessionCreated(HttpSessionEvent hse) {
		System.out.println(hse);
        sessions.put(hse.getSession().getId(), hse.getSession());
		
	}

	@Override
	public void sessionDestroyed(HttpSessionEvent hse) {
		if(sessions.get(hse.getSession().getId()) != null){
            sessions.get(hse.getSession().getId()).invalidate();
            sessions.remove(hse.getSession().getId());	
        }		
	}
	
    
	//중복로그인 지우기   
    public synchronized static String getSessionidCheck(String type, String compareId){
		String result = "";
		for( String key : sessions.keySet() ){
			HttpSession hs = sessions.get(key);
			if(hs != null &&  hs.getAttribute(type) != null && hs.getAttribute(type).toString().equals(compareId) ){
				result =  key.toString();
			}
		}
		removeSessionForDoubleLogin(result);
		return result;
	}
    
    private static void removeSessionForDoubleLogin(String userId){    	
        System.out.println("remove userId : " + userId);
        if(userId != null && userId.length() > 0){
            sessions.get(userId).invalidate();
            sessions.remove(userId);    		
        }
    }
    
    public static int getSessionCnt() {
    	return sessions.size();
    }
    
    public static Map<String, HttpSession> getSessionList() {
    	return sessions;
    }    
}

@WebListener 어노테이션을 통해 리스너임을 알린다.

ConcurrentHashMap을 사용하여 세션을 처리하는데,

ConcurrentHashMap는 일반 HashMap과는 다르게 key, value값으로 Null을 허용하지 않는 컬렉션입니다.

 

@WebListener를 사용하지않을 경우에는 web.xml 파일에 리스너로 등록해준다.

<listener>
	<listener-class>패키지경로.CustomHttpSessionListener</listener-class>
</listener>

getSessionCheck메소드에 synchronized키워드를 사용하여 순차적인 처리를 진행한다. synchronized를 사용하는 이유는 멀티쓰레드로 인한 동시 접근을 막아 처리의 순서를 보장하기위해 해당 키워드를 사용한다. 톰캣은 접속하는 세션이 늘어날 때마다 쓰레드가 증가된다.

 

세션이 끊기면 자동으로 로그인페이지로 이동처리 하는 방법

invalid-session-url 속성값을 지정해주면 코드상에서 별도의 작업하지않아도 자동으로 설정한 url로 이동한다고 

하는데 그렇게 되지않았다.

<s:session-management invalid-session-url="/login.do">
    <s:concurrency-control max-sessions="1" expired-url="/login.do" error-if-maximum-exceeded="true"/>
</s:session-management>

 

리스너로 등록된 클래스 파일에 접근하는 방법은 다음과 같이 접근하여 정보를 수집할 수 있다.

Map<String, HttpSession> sessionsList = CustomHttpSessionListener.getSessionList();

 

 

[JAVA] 세션정보(session)를 가져오는 방법

1. 세션정보 가져오는 방법 ServletRequestAttributes servletRequestAttribute = (ServletRequestAttributes) RequestContextHolder.currentRequestAttributes(); HttpSession httpSession = servletRequestAttri..

playground.naragara.com

 

[연관 이슈]

동시 세션 제어 – 동일 브라우저에서 로그아웃이 정책 미적용 – 인프런 | 질문 & 답변 (inflearn.com)

 

동시 세션 제어 – 동일 브라우저에서 로그아웃이 정책 미적용 – 인프런 | 질문 & 답변

안녕하세요. 강좌를 유익하게 잘 보고 있습니다. 동시 세션 제어 부분에서 해당 Security 2.3.1 버전을 사용하고 있는데  ‘maxSessionsPreventsLogin(true)’ 상태에서 아래와 같은 동작 시  ‘Maximum sessions …

www.inflearn.com

 

[REFERENCE]

 

 

 

 

 

 

Leave a Reply

error: Content is protected !!