[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();
}
전자정부프레임워크 포털 사이트에서 묻고 답하기 탭을 이용하여 “로그아웃”으로 검색시 많은 질의답변결과를 참고하면 도움이 될 수 있다.
참고로, 브라우저를 닫거나 alt+F4와 같은 방식으로 종료하는 경우 현재 HTTP로 서비스되는 처리 상 connectionless 방식이기 때문에 세션 만료 처리는 불가능하다.
스프링 시큐리티를 사용하지 않고 중복로그인을 해결하는 방법
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();
[연관 이슈]
동시 세션 제어 – 동일 브라우저에서 로그아웃이 정책 미적용 – 인프런 | 질문 & 답변 (inflearn.com)
[REFERENCE]
- https://www.egovframe.go.kr/home/qainfo/qainfoRead.do?menuNo=69&qaId=QA_00000000000018677
- 중복로그인 차단 문의 | 묻고 답하기 | 표준프레임워크 포털 eGovFrame
- https://dlgkstjq623.tistory.com/317
- Spring Security 다중 로그인 화면 적용 : 네이버 블로그 (naver.com)
- Spring 중복로그인 방지. 관리자쪽에서 중복로그인을 불가능하게 해달라는 요청이 들어왔다.
- Spring – HttpSessionListener 로그인 세션 관리(중복 로그인 방지하기)
- Spring Security – 한 유저 동시 접속 막기
- 스프링시큐리티 중복로그인(동시접속) 제한처리 – 다중서버환경 (tistory.com)
- https://offbyone.tistory.com/236
- http://parkminkyu.github.io/%EC%A4%91%EB%B3%B5-%EB%A1%9C%EA%B7%B8%EC%9D%B8-%EB%B0%A9%EC%A7%80-or-%EC%A4%91%EB%B3%B5%EC%84%B8%EC%85%98-%EC%A0%9C%ED%95%9C%ED%95%98%EA%B8%B0/
- https://stackoverflow.com/questions/15011498/spring-security-redirect-when-maximum-sessions-for-this-principal-exceeded
- [Spring] HttpSessionListener 이용해 동시 로그인 사용자 및 세션 관리
- egovframework:rte3:fdl:server_security:xmlschema_v3_7 [eGovFrame]
- egovframework:com:v3:cmm:multilogin [eGovFrame]
- 중복 로그인 방지 in Session Clustering Env (dveamer.github.io)
- spring security 파헤치기 (구조, 인증과정, 설정, 핸들러 및 암호화 예제, @Secured, @AuthenticationPrincipal, taglib)
- 스프링 시큐리티 기본 API및 Filter 이해 (oopy.io)
- https://jaimemin.tistory.com/1691
- https://jason-moon.tistory.com/142
- https://stackoverflow.com/questions/52838124/applicationlistenersessiondestroyedevent-is-not-called
- https://okky.kr/article/342715
- https://www.inflearn.com/questions/40072
- https://escapefromcoding.tistory.com/487
- https://velog.io/@devsh/%EC%8A%A4%ED%94%84%EB%A7%81-%EC%8B%9C%ED%81%90%EB%A6%AC%ED%8B%B0-%EC%84%B8%EC%85%98-Session-Management-%EC%BB%A4%EC%8A%A4%ED%84%B0%EB%A7%88%EC%9D%B4%EC%A7%95-%EA%B0%9C%EB%85%90-%EC%9D%B5%ED%9E%88%EA%B8%B0
- https://sangmoo.tistory.com/217