백앤드 이야기/JAVA&Spring

[Spring] HttpSessionListener 이용해 동시 로그인 사용자 및 세션 관리

한희성 2024. 3. 12.
반응형

쿠팡 모든 상품 최저가 보러가기 => https://damoareview.store/

 

다모아 리뷰!

2024년 최고 동원참치선호 베스트8 안녕하세요. 2024년 최고 동원참치선호 베스트8에 대해서 추천해드리겠습니다.제품별 스펙과 가격대, 사용 후기까지 꼼꼼하게 비교해보며 현명한 구매 결정을

damoareview.store

(쿠팡 파트너스 활동을 통해 일정액의 수수료를 제공받을 수 있습니다.)

 

 

안녕하세요 깐지꾼지빠 입니다!

 

얼마전에 울 둥이들 드디어 !! 오지 않을 것만 같았던 돌이 지났습니다 !! 짝짝짝(광고클릭..)

 

얼마전이라고 해도 사실 한달이 지났시점이라.. 7월 중순에 잔치 했습니다..(그때는 부담만 있었지.. 위험까진 덜했죠 ㅠㅠ)

 

본론으로 들어와서 오늘 작성할 포스팅 주제는 HttpSessionListener 이용하여 동시 접속 로그인 유저 확인 및 유

 

저가 이탈한 시간 ! 세션관리! 입니다.

 

 

* 개발환경

- 인텔리J 2020. 1

- JAVA8

- Spring Boot 2.3.2

- Gradle-6.4.1

 

 

1. 

HttpSessionListener 상속

/**
 * session.setAttribute 실행 되는 순간 같은 sessionId 일경우 1회만 실행
 * @param httpSessionEvent
 */
@Override
public void sessionCreated(HttpSessionEvent httpSessionEvent) {
    log.info("sessionCreated -> {}", httpSessionEvent.getSession().getAttribute("userId"));
}

/**
 * session 이 소멸되는 시점에 실행, 기본 단위는 초(1분 미만은 설정할 수 없음)
 * @param httpSessionEvent
 */
@Override
public void sessionDestroyed(HttpSessionEvent httpSessionEvent) {
    log.info("sessionDestroyed 실행");
    HttpSession session = httpSessionEvent.getSession();

    String userId = (String) session.getAttribute("userId");

    //로그아웃 유저 삭제
    synchronized(loginSessionList){
        loginSessionList.remove(httpSessionEvent.getSession().getId());
    }

    if(userId != null){
        this.updateUserCloseTime(userId);
    }

    currentSessionList();
}

- 리스너를 상속 받았다면 구현 해야하는 구현체는 두가지 입니다.

- sessionCreated, sessionDestroyed

* sessionCreated : 이 메서드가 호출되는 시점은 session 이 생성되는 시점으로, 동일 sessionId 에서는 1회만 호출됩니다.

* sessionDestroved : 마찬가지로 세션이 invaild 되는 시점에 호출되며, setMaxInactiveInterval 에 지정해 놓은 세션 타임아웃이 다 하여도 동일하게 호출됩니다.

 

 

2. WebSessionListener 전체 소스

package com.heeseong.session.weblistener;

import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpSession;
import javax.servlet.http.HttpSessionEvent;
import javax.servlet.http.HttpSessionListener;
import java.util.Enumeration;
import java.util.Hashtable;


@Slf4j
@Component
public class WebSessionListener implements HttpSessionListener {

    public static WebSessionListener sessionListener = null;
    private static Hashtable loginSessionList = new Hashtable();

    /**
     * 싱글톤 생성
     * @return
     */
    public static synchronized WebSessionListener getInstance() {
        if(sessionListener == null) {
            sessionListener = new WebSessionListener();
        }
        return sessionListener;
    }

    /**
     * session.setAttribute 실행 되는 순간 같은 sessionId 일경우 1회만 실행
     * @param httpSessionEvent
     */
    @Override
    public void sessionCreated(HttpSessionEvent httpSessionEvent) {
        log.info("sessionCreated -> {}", httpSessionEvent.getSession().getAttribute("userId"));
    }

    /**
     * session 이 소멸되는 시점에 실행, 기본 단위는 초(1분 미만은 설정할 수 없음)
     * @param httpSessionEvent
     */
    @Override
    public void sessionDestroyed(HttpSessionEvent httpSessionEvent) {
        log.info("sessionDestroyed 실행");
        HttpSession session = httpSessionEvent.getSession();

        String userId = (String) session.getAttribute("userId");

        //로그아웃 유저 삭제
        synchronized(loginSessionList){
            loginSessionList.remove(httpSessionEvent.getSession().getId());
        }

        if(userId != null){
            this.updateUserCloseTime(userId);
        }

        currentSessionList();
    }

    /**
     * 현제 HashTable에 담겨 있는 유저 리스트, 즉 session list
     */
    private void currentSessionList(){
        Enumeration elements = loginSessionList.elements();
        HttpSession session = null;
        while (elements.hasMoreElements()){
            session = (HttpSession)elements.nextElement();

            String userId = (String)session.getAttribute("userId");
            log.info("currentSessionUserList -> userId {} ", userId);
            //log.info("currentSessionUserList -> sessionId {} ", session.getId());
            //log.info("currentSessionUserList -> hashtable SessionList {} ", loginSessionList.get(session.getId()));
        }
    }

    /**
     * session 생성
     * @param request
     * @param value
     */
    public void setSession(HttpServletRequest request, String value){
        log.info("setSession 실행");
        HttpSession session = request.getSession();
        session.setAttribute("userId", value);
        session.setMaxInactiveInterval(2);

        //HashMap에 Login에 성공한 유저 담기
        synchronized(loginSessionList){
            loginSessionList.put(session.getId(), session);
        }
        currentSessionList();
    }

    /**
     * session 삭제
     * 세션이 remove 되면 destroyed 함수 실행
     * @param request
     */
    public void removeSession(HttpServletRequest request){
        log.info("removeSession 실행");

        HttpSession session = request.getSession();
        String userId = (String)session.getAttribute("userId");

        session.removeAttribute("userId");
        session.invalidate();

        if(userId != null){
            this.updateUserCloseTime(userId);
        }
    }

    /**
     * 유저 나간 시간 업데이트
     * @param userId
     */
    private void updateUserCloseTime(String userId) {
        log.info("updateUserCloseTime {} ", userId);
        //호출부에서 NULL 검사
        //업데이트 로직
    }

    /**
     * 현재 로그인한 유저가 이미 존재 하는지 확인
     * @param request
     * @param loginUserId
     * @return boolean
     */
    public boolean isLoginUser(HttpServletRequest request, String loginUserId){
        Enumeration elements = loginSessionList.elements();
        HttpSession session = null;
        while (elements.hasMoreElements()){
            session = (HttpSession)elements.nextElement();
            String userId = (String)session.getAttribute("userId");
            if(loginUserId.equals(userId) && (!session.getId().equals(request.getSession().getId()))){
                return true;
            }
        }
        return false;
    }

}

* Hashtable : 해쉬 테이블을 이용해 로그인한 유저들은 맵에 넣어서 관리하였으며,

* isLoginUser 함수 : 해쉬 테이블 안에 들어있는 엘리먼츠 들을 꺼내서 다음에 들어온 유저와 비교하여 sessionId 가

    다른데 userID가 동일할 경우 동시접속으로 판단하여 true 를 return 하도록 하였습니다.

 

 

3. View 화면에서 유저가 나간 시간체크하기

 

* 구현에 앞서 가장 까다로웠던 로직이 아닐까 싶습니다. 고려해야 할 것들이 너무 많았습니다 ㅠㅠ

* 화면에서 이탈하는 경우 beforeunload 로 캐치하여 잡아내면 되지만, 고려해야 할것들이 무려..

- 새로고침(F5, Alt+R, 우클릭 새로고침)

- Alt + F4

- Ctrl + W

- 마우스로 닫기

   이만큼이나 고려해야 하기 때문에 어떻게 발라내서 처리할까 고민을 많이 했습니다.

 

beforeunload  에 대한 스펙은 https://developer.mozilla.org/ko/docs/Web/API/Window/beforeunload_event

 

 

4. View 전체 소스 

<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
    <script src="http://code.jquery.com/jquery-latest.min.js"></script>
</head>
<body>
인덱스 입니다. 버튼이 하나면 들어올때마다 눌립니다 body 에 input button 이 하나라서..
<input type="button" value="로그아웃" onclick="goLogout()"/>
<input type="button" value="걍버튼" />
<script>

    var flag = true;

    $(window).on('beforeunload', function() {
        return windowClose();
    });

    $(document).keydown(function(e) {
        var key = (e) ? e.keyCode : event.keyCode;
        //alert(key)
        if(e.ctrlKey){
            //이걸로 컨트롤키 뽑을 수 있음
        }
        if(e.altKey){
            //이걸로 알트키 뽑을 수 있음
        }
        if(e.ctrlKey && e.keyCode == 82){
            flag = false;
            console.log("컨트롤 + R");
        }
        if(e.keyCode == 116){
            flag = false;
            console.log("F5");
        }
        if(e.keyCode == 115){
            console.log("F4");
        }
        // ctrl = 17
        // alt = 18
        // r == 82
        // f4 = 115
        // f5 = 116
        // backspace = 8
    });

    //새로고침이나 X 표시 누르면 무조건 동작
    //return String 을 할 경우 멈춰 세울순 있으나 동작 자체는 무조건 하므로 막을 방법이 없음
    //F5, ctrl + r 일때 빼고 모든 동작 시켜야 할듯
    //https://developer.mozilla.org/ko/docs/Web/API/Window/beforeunload_event
    var windowClose = function(e){
        var agent = navigator.userAgent.toLowerCase();
        console.log(flag);
        if(flag){
            console.log("로그아웃");
            goLogout();
        }else{
            console.log("새로고침");
        }

        /*if(agent.indexOf('chrome')!= -1){
            console.log("크롬");
            //return "종료 하시겠습니까?";
        }else{
            console.log("크롬 외 브라우저");
            //return "종료 하시겠습니까?";
        }*/
    }
    setInterval(keepAlive,1000);

    function keepAlive(){
        $.ajax({
            type:"POST"
            , dataType:'text'
            , url:"/test/was/keepAlive"
            , success : function(data){

            },error:function(x,error){
                alert("처리중 오류가 발생했습니다.");
            }
        });
    }

    function goLogout(){
        $.ajax({
            type:"POST"
            , dataType:'text'
            , url:"/test/logout"
            , success : function(data){

            },error:function(x,error){
                alert("처리중 오류가 발생했습니다.");
            }
        });
    }

    $(document).on('mousedown', function() {
        if ((event.button == 2) || (event.which == 3)) {
            console.log("마우스 우클릭");
        }
    });
</script>
</body>
</html>

 

* beforeunload 는 말그대로 언로드.. 새로고침, 화면 닫힘등에 무조건 동작하는 함수입니다 !! 제가 원하는 로직은 유저

 

새로고침을 했을 땐 종료시간을 캐치 하지 않는 것이기 때문에 key 이벤트를 이용해 새로고침 키와, 컨트롤+R 키를 눌렀

 

을 때는 logout 로직을 타지 않게 처리 했습니다. 그렇게 되면 새로고침 이후 입장시간만 있을 뿐 종료 시간은 없기 때문

 

에 sum 이 가능하지요 ^^

 

또한 Alt + F4 경우에도 언로드 함수가 동작하기 때문에 종료시간 캐치 가능!!! 하지만 또다른 문제는 모바일에서 문제

 

가 있습니다 ㅋㅋ 홈키 버튼을 이용해 화면이 내려가거나 그냥 종료한경우(피시 전원 내려가는경우랑 동일)엔 캐치를 할

 

수가 없기 때문에 최대한 종료시간을 근사치로 구하기 위해 세션시간을 짧게하고 킵얼라이브 기능을 만들었죠 ~

 

예를들어 세션시간이 5분이면, 킵 얼라이브타임을 2분으로 잡아두고 주기적으로 신호를 보내서 세션타임아웃을 갱신!!

 

홈버튼 또는 PC 가 예상치 못하게 종료 되었을경우에도 최대 5분이 차이가 나므로 유저가 종료한 시간을 정확히 5분 근

 

사치 이내로 구할 수 있습니다!!! 이것저것 삽질하면서 공부한 기능이라서.. 꽤나 도움이 많이 되었으면 좋겠습니다.!

 

 

반응형

댓글

💲 추천 글