Programming > Spring Framework

다중로그인 금지 (웹서버)

#다중접속금지,#이중로그인금지#세션공유#아이디공유#이중접속#다중접속

로드밸런싱, 이중화, 메모리 세션만으로 다중 로그인 체크가 어려울 경우, 테이블에 사용자 세션정보를 등록해 체크하는 방법

1) 세션로그인 정보를 등록하는 테이블을 생성한다.

  1. USER_ID : 사용자 ID
  2. SESSION_ID : 시스템에 로그인 성공 후 생성된 세션ID에 해당한다.
  3. ON_YN : 현재 로그인중일 경우(Y), 로그인이 아닌상태(N)
  4. ST_DT : 현재 로그인중일 경우, 로그인한 시각을 나타낸다.
  5. ED_DT : 로그아웃한 시각 (이전 로그인에서 로그아웃한 시각)

 

다중로그인 체크 테이블 : LOGIN_SESSION
컬럼 타입 Null default 내용
USER_ID VARCHAR2(50) PK   사용자ID
SESSION_ID VARCHAR2(128) N   세션ID
ON_YN CHAR(1) N 'Y' 로그온중 여부
CHECK_YN CHAR(1) N 'Y' 이중로그인 체크 적용여부
LOGON_IP VARCHAR2(128) N   로그인 IP
ST_DT DATE N SYSDATE 로그인 시간
ED_DT DATE N SYSDATE 로그아웃 시간

 

2) 세션리스너를 추가한다. (로그아웃)

ⓐ web.xml 또는 설정에 세션리스너를 등록한다.

<listener>
           <listener-class>com.emunhi.config.SessionConfig</listener-class>
</listener>   

ⓑ create Context Class

@Component
public class AppContext implements ApplicationContextAware {
    private static ApplicationContext context;

    @Override
    public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
        context = applicationContext;
    }

    public static ApplicationContext getContext() {
        return context;
    }

    @SuppressWarnings("unchecked")
    public static <T> T getInstance(String className) {
        return (T)getContext().getBean(className);
    }
}
 

ⓒ HttpSessionListener 구현 클래스를 생성한다.

    ※ 세션아웃(logoutSession)메소드 에서 로그인중일 경우, 해당 세션 파기와 동시에 테이블에서 로그아웃플래그를 설정한다.

public class SessionConfig implements HttpSessionListener, Serializable {
          
           private static final long serialVersionUID = -4971790522293467330L;

           @Override
           public void sessionCreated(HttpSessionEvent event) {
           }
           @Override
           public void sessionDestroyed(HttpSessionEvent event) {
                     logoutSession(event);
           }
           /** 세션아웃   */
    private void logoutSession(HttpSessionEvent event){
        HttpSession session = event.getSession();
       
        SessionLoginVo svo = (SessionLoginVo)session.getAttribute(“세션빈”);
        if(svo==null || svo.getUserId()==null) {
                  // 세션이 없으면 스킵한다.
                  return;
        }
        String userId = svo.getUserId();
        String sessionId = session.getId();
                
        UserMapper mapper = AppContext.getInstance("userMapper");
        Param param = new Param();
        param.put("userId"userId);
        param.put("sessionId", sessionId);
        mapper.updateLogoutSession(param);
    }
}

  ※ 로그아웃 쿼리

<update id="updateLogoutSession" parameterType="param">
           UPDATE LOGIN_SESSION SET
                      ON_YN = 'N'
                     ,ED_DT = SYSDATE
           WHERE CHECK_YN = 'Y' AND USER_ID = #{userId} AND SESSION_ID = #{sessionId}
</update>

3) 로그인 (세션생성)

  ※ 로그인 성공시 세션정보를 테이블에 등록한다.

// 세션로그인 등록
Param pm = new Param();
pm.put("userId", userId);
pm.put("sessionId", request.getSession().getId());
pm
.put("logonIp", request.getRemoteAddr());

memberLoginDAO.mergeLoginSession(pm);


<!-- 세션등록 -->
<update id="mergeLoginSession" parameterType="param">
           MERGE INTO LOGIN_SESSION A
           USING (SELECT #{userId} AS USER_ID FROM DUAL) B
                     ON (A.USER_ID = B.USER_ID)
           WHEN MATCHED THEN
                     UPDATE SET
                                 SESSION_ID = #{sessionId}
                                ,ON_YN = 'Y'
                                ,LOGON_IP = #{logonIp}

                                ,ST_DT = SYSDATE
           WHEN NOT MATCHED THEN
                     INSERT (USER_ID, SESSION_ID, LOGON_IP)
                     VALUES ( #{userId}, #{sessionId}, #{logonIp})
</update>

4) 다중로그인 체크

  ※ 다중로그인 체크는 현재 나의 세션에 대해 인터셉터등에서 상시 체크해 타세션에서 로그인 중인가 확인

※ 인터셉터에서 로그인 세션에 대해 다음을 실행한다.
UserMapper
mapper = AppContext.getInstance("userMapper");


Param user = mapper.getLoginSession(userInfo.getUserId());
if(user == null ||
    ( "Y".equals(user.get("checkYn")) &&
        (
            "N".equals(user.get("onYn")) || !user.get("sessionId").equals(request.getSession().getId())
        )
    )
){
    // 현세션에 대한 강제로그아웃
    request.getSession().invalidate();
   
    // 정상적인 세션정보가 없으면 로그인페이지로 이동
    ModelAndView mav = new ModelAndView();
    mav.addObject("message", "이중 로그인으로 로그아웃 처리 되었습니다. (사용중 : " + user.get("logonIp") + ")");
    mav.setViewName("/login/alert");
    throw new ModelAndViewDefiningException(mav);
}

 

<!-- 중복로그인 금지 처리 -->
<update id="updateLogoutSession" parameterType="EgovMap">
    UPDATE LOGIN_SESSION SET
         ON_YN = 'N'
        ,ED_DT = SYSDATE
    WHERE CHECK_YN = 'Y' AND USER_ID = #{userId} AND SESSION_ID = #{sessionId}
</update>
<update id="mergeLoginSession" parameterType="EgovMap">
    MERGE INTO LOGIN_SESSION A
    USING (SELECT #{userId} AS USER_ID FROM DUAL) B
        ON (A.USER_ID = B.USER_ID)
    WHEN MATCHED THEN
        UPDATE SET
            SESSION_ID = #{sessionId}
           ,ON_YN = 'Y'
           ,LOGON_IP = #{logonIp}
           ,ST_DT = SYSDATE
    WHEN NOT MATCHED THEN
        INSERT (USER_ID, SESSION_ID, LOGON_IP)
        VALUES ( #{userId}, #{sessionId}, #{logonIp})
</update>
<select id="isNoLoginSession" parameterType="String" resultType="java.lang.Integer">
           SELECT COUNT(1) FROM LOGIN_SESSION
           WHERE SESSION_ID = #{sessionId} AND ON_YN = 'Y' AND CHECK_YN = 'Y'
</select>
<select id="getLoginSession" parameterType="String" resultType="EgovMap">
           SELECT
                     USER_ID, SESSION_ID,ON_YN, CHECK_YN, LOGON_IP
           FROM LOGIN_SESSION
           WHERE USER_ID = #{userId}
</select>