[SPRING]/Security

중복로그인(동시접속) 방지 수정기(security, AuthenticationSuccessHandler)

뽀준 2023. 6. 28. 17:31
반응형

중복로그인 방지 수정기(security, AuthenticationSuccessHandler)

보안 관련 수정을 진행하며 관리자 사이트에 중복로그인을 막아달라는 요청이있었다.
스프링에서 다들 많이 이용하는 spring security를 이용하는 서비스였고 security 설정 파일에 설정 추가해주는것으로 끝날 줄 알았다.

security 설정 파일에 설정 추가

보통 security-context.xml 또는 자신이 설정한 설정파일에 설정을 추가해주면된다.
이 프로젝트의 경우에는 application-context.xml안에 설정이 되어있었다.
어느 파일에 설정을 하던 xml파일의 상단에

<beans xmlns="http://www.springframework.org/schema/beans"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xmlns:security="http://www.springframework.org/schema/security"
    xmlns:tx="http://www.springframework.org/schema/tx"
    xmlns:context="http://www.springframework.org/schema/context"
    xmlns:task="http://www.springframework.org/schema/task"
        xsi:schemaLocation="http://www.springframework.org/schema/security http://www.springframework.org/schema/security/spring-security-5.3.xsd
        http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
        http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx-4.3.xsd
        http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd
        http://www.springframework.org/schema/task http://www.springframework.org/schema/task/spring-task.xsd">

스키마 추가하고 불러오기 때문에 자신의 프로젝트에 맞는 부분을 찾으면된다.

설정을 읽어보는데 이미 security에 중복로그인 방지 설정이 되어있었다.

<security:http auto-config="true" use-expressions="true" entry-point-ref="ajaxAuthenticationEntryPoint">
...
    <security:session-management invalid-session-url="/login">
        <security:concurrency-control max-sessions="1" expired-url="/login" />
    </security:session-management>
...
</security:http>

응? 설정되어있는데 왜 중복로그인이 가능하지? 이런 상황이면 로그인 프로세스를 뜯어봐야한다...ㅠㅠ

로그인 프로세스 뜯어보기

인증 - 인가 세션 방식으로 되어있고 인증관련해 AuthenticationSuccessHandler를 상속받아 이용하고 있었다.
우리가 해야할 부분은 중복로그인을 막는 부분을 구현해야하므로 인증 성공 후 세션에서 체크해 이미 인증된 세션이 있는 경우 해당 세션을 끊어주면된다.
코드를 확인해보니 인증 성공 후 로그인 이력을 남기는 부분과 어느 페이지로 이등시킬지에 대한 부분만 구현되어있었다.

자 그럼 절차는 다음과 같다.

  1. 인증성공시 사용자를 담을 세션 객체 선언
  2. 인증성공 후 수행되는 절차 중 마지막에 성공한 사용자를 세션 객체에 담아주기 구현
  3. 인증성공 후 수행되는 절차 중 앞부분에 성공한 사용자가 이미 세션에 들어있는지 찾고 있다면 해당 사용자 로그아웃

구현

// 세션객체 생성
private static final ConcurrentHashMap<String, HttpSession> activeSessions = new ConcurrentHashMap<>();

@Override
public void onAuthenticationSuccess(HttpServletRequest req, HttpServletResponse res, Authentication authentication) throws IOException, ServletException {
    LoginUserDto userDto = (LoginUserDto) authentication.getPrincipal();
    RedirectStrategy redirectStrategy = new DefaultRedirectStrategy();
    String ip = req.getHeader("X-FORWARDED-FOR");
    if (ip == null) {
        ip = req.getRemoteAddr();
    }

    // 사용자가 세션에 있는지 확인
    if (checkUserAlreadyLoggedIn(userDto.getUsername())) {
        // 세션에 있는 기존 사용자 끊기
        invalidatePreviousSessions(req, res, userDto.getUsername());
    }

    // 로그인 성공한 사용자의 세션을 세션 목록에 추가
    activeSessions.put(userDto.getUsername(), session);

    // 기존 기능들 ...
    // 로그인 기록 삽입 ...
    // 적절한 페이지로 리다이렉트 ...
}

// 사용자가 이미 로그인되어 있는지 확인
private boolean checkUserAlreadyLoggedIn(String username) {
    for (String loggedInUser : activeSessions.keySet()) {
        if (loggedInUser.equals(username)) {
            return true;
        }
    }
    return false;
}

// 기존 세션 무효화
private void invalidatePreviousSessions(HttpServletRequest req, HttpServletResponse res, String username) {
        // 현재 사용자의 모든 세션 무효화
        Iterator<Map.Entry<String, HttpSession>> iterator = activeSessions.entrySet().iterator();
        while (iterator.hasNext()) {
            Map.Entry<String, HttpSession> entry = iterator.next();
            if (entry.getKey().equals(username) && !entry.getValue().getId().equals(req.getSession().getId())) {
                HttpSession session = entry.getValue();
                try {
                    session.invalidate();
                    logger.info("기존 세션 로그아웃: " + username);
                } catch (IllegalStateException e) {
                    // 세션이 이미 무효화된 경우에 대한 예외 처리
                    logger.error("세션 무효화 중 예외 발생: " + e.getMessage());
                }
                iterator.remove();
                break; // 루프 종료
            }
        }
    }
반응형