[Spring Security] 중복 로그인 방지 및 알림 표시하기

Doyeon·2023년 9월 2일
0
post-thumbnail

사이버 보안 관련으로, 다중 접속 시 기존 로그인 계정을 자동으로 로그아웃 시키고 알림을 표시해주는 기능을 개발하게 되었다.
현재 프로젝트는 스프링 시큐리티를 사용 중이어서, 스프링 시큐리티에서 제공해주는 세션 관리 메서드를 활용해서 개발을 진행했다.
세션이 만료되었을 때 처리 방법을 정의한 클래스를 만들어 적용하여 기능을 구현해보았다.
기존 로그인 세션을 자동으로 로그아웃 시키는 것은 금방 구현했는데, 기존 로그인 사용자에게 알림을 어떻게 보내야 될 지 고민을 많이 했다.
나름대로 풀어낸 고민의 결과를 지금부터 기록해본다.

설계

  • Spring Security 설정에 sessionManagement() 세션 관리 메서드를 사용하여, 한 사용자가 한번에 하나의 세션만 가질 수 있도록 설정한다.
  • 로그인 되어 있는 상태에서 새로운 로그인을 시도할 경우, 기존 로그인 세션이 만료되고 새 로그인이 성공된다.
  • 세션 만료 시 처리 방법은 SessionInformationExpiredStrategy 를 구현한 커스텀 클래스에 정의한다.
  • 기존 로그인 세션이 만료될 때, request 속성 값에 DUPLICATE_LOGIN = true 를 추가한 후, login 페이지로 이동한다.
  • 기존 로그인 세션에서 “다른 기기에서 로그인되어 현재 로그인이 종료되었습니다.” 알림창 안내 후, 로그인 페이지를 출력한다.

구현

SecurityConfiguration.java

@Configuration
public class SecurityConfiguration extends WebSecurityConfigurerAdapter {

		@Override
    protected void configure(HttpSecurity http) throws Exception {

				// ...

				http.sessionManagement()
				        .sessionFixation().changeSessionId()
				        .maximumSessions(1)
				        .expiredSessionStrategy(customSessionExpiredStrategy)
				        .maxSessionsPreventsLogin(false)
				        .sessionRegistry(sessionRegistry());
		}

		// ...

		@Bean
		public SessionRegistry sessionRegistry() {
		    return new SessionRegistryImpl();
		}
		
		@Bean
		public static ServletListenerRegistrationBean<HttpSessionEventPublisher> httpSessionEventPublisher() {
		    return new ServletListenerRegistrationBean<>(new HttpSessionEventPublisher());
		}
}
  • Spring Security Config - http.sessionManagement() 설정 추가
    • sessionManagement() : Spring Security에서 세션 관리를 구성하는 데 사용되는 메서드로, 중복 로그인 방지, 세션 무효화 전략, 최대 동시 세션 수 등과 같은 세션 관련 설정을 처리한다.
    • sessionFixation().chagesSessionid() : 사용자 인증이 성공하며 세션 ID를 변경하여 기존 세션을 무효화하고 새로운 세션을 생성한다. 세션 고정 공격을 방어할 수 있다.
    • maximumSessions(1) : 동시에 허용되는 세션 수를 설정한다. 한 사용자당 허용되는 최대 세션 수를 의미한다. maximumSessions(1) 을 설정하면 한 사용자가 한 번에 하나의 세션만 가질 수 있기 때문에, 이미 로그인한 상태에서 다른 장치나 브라우저로 로그인하려고 하면 기존 세션은 만료되고 새로운 세션으로 대체된다.
    • expiredSessionStrategy(customSessionExpiredStrategy) : 세션이 만료되었을 때 어떻게 처리할지 정의한다. customSessionExpiredStrategy 에서 정의한 대로 처리된다.
    • maxSessionPreventsLogin(false) : 동시에 로그인한 세션 수가 maximumSessions() 로 설정한 값에 도달했을 때, 새로운 로그인 시도를 허용할 지 여부를 설정한다.
      • ‘true’ : default는 true이며, 동시 로그인 세션 수가 최대값에 도달하면 새로운 로그인 시도가 차단된다.
      • ‘false’ : 동시 로그인 세션 수가 최대값이어도 새로운 로그인을 차단하지 않으며, 기존 로그인 세션 중 하나가 로그아웃되거나 만료되면 새로운 로그인 세션이 허용된다.
    • sessionRegistry(sessionRegistry()) : 동시에 로그인한 세션들을 추적하고 관리한다.
  • SessionRegistry , HttpSessionEventPublisher 컴포넌트 추가
    • SessionRegistry : 중복 로그인 방지를 위해 동시에 여러 세션이 열리지 않도록 한다.
    • HttpSessionEventPublisher : HttpSession 이벤트를 Spring 이벤트로 변환시킨다. 세션 생성 및 소멸 이벤트를 처리하는 데 도움이 된다.

CustomSessionExpiredStrategy.java

  • [방법 1] session 속성 값 추가 → response.sendRedirect("/login");
    @Component
    public class CustomSessionExpiredStrategy implements SessionInformationExpiredStrategy {
    
        @Override
        public void onExpiredSessionDetected(SessionInformationExpiredEvent event) throws IOException, ServletException {
            HttpServletRequest request = event.getRequest();
            HttpServletResponse response = event.getResponse();
            HttpSession session = request.getSession();
    
            session.setAttribute("DUPLICATE_LOGIN", true);
    
            response.sendRedirect("/login");
        }   
    }
    • 웹 브라우저에 새로운 URL로 다시 요청하므로, 이전 요청과는 별개의 요청으로 처리된다.
    • 새로운 요청이 발생하므로, 이전 페이지(또는 서블릿)에서 전달한 데이터가 사라진다. 이전 요청의 request.setAttribute로 설정한 속성 값에 접근할 수 없다.
    • 새 요청에서도 속성 값을 사용할 수 있도록 세션에 속성을 넣어 속성값을 사용했다.
    • 하지만, 세션 사용하는 경우, jsp에서 속성 값에 따른 동작 처리 후, 세션 값을 초기화 해주어야 한다.
  • [방법 2] request 속성 값 추가 → request.getRequestDispatcher("/login").forward(request, response);
    @Component
    public class CustomSessionExpiredStrategy implements SessionInformationExpiredStrategy {
    
        @Override
        public void onExpiredSessionDetected(SessionInformationExpiredEvent event) throws IOException, ServletException {
            HttpServletRequest request = event.getRequest();
            HttpServletResponse response = event.getResponse();
    
            request.setAttribute("DUPLICATE_LOGIN", true);
    
            request.getRequestDispatcher("/login").forward(request, response);
        }   
    }
    • 현재의 서블릿 내에서 다른 JSP로 제어를 전달하므로, 웹 브라우저에는 새로운 요청이 발생하지 않는다. 내부적으로 서버 내에서만 이동하여 URL은 변경되지 않는다.
    • 이동하는 페이지(또는 서블릿)에서 이전 페이지에서 전달한 데이터와 요청 객체를 공유한다.
    • request에 “DUPLICATE_LOGIN” 속성을 추가하여 사용했다.
    • 로그인 화면으로 동작하는 모든 과정이 끝나면, 이후 로그인 페이지 요청은 새로운 요청이기 때문에 request 속성 “DUPLICATE_LOGIN”을 초기화하지 않아도 된다.
  • [방법 2]를 사용하여 request 속성에 DUPLICATE_LOGIN 값을 추가하여 구현했다. 서버 내에서 페이지만 이동하도록 구현하면 request 속성 값을 이동한 페이지에서도 사용할 수 있고, 세션처럼 따로 초기화를 하지 않아도 되기 때문에 더 효율적이라고 판단했다.

CustomUserDetails.java

public class CustomUserDetails implements UserDetails {

		// ... 
		
		@Override
		public boolean equals(Object obj) {
		    if (obj instanceof CustomUserDetails) {
		        return this.username.equals(((CustomUserDetails) obj).username);
		    }
		    return false;
		}
		
		@Override
		public int hashCode() {
		    return this.username.hashCode();
		}
		
}
  • equals, hash 메서드 추가
    • spring security 의 중복 로그인 은 userDetail 구현 객체인 User 객체가 같은지 체크 후, 같은 유저가 존재하는 경우에 중복 로그인 처리를 진행하도록 되어있다.
    • 따라서 userDetails를 custom하게 구현했다면 반드시 equals, hash 등의 메서드에 대한 처리가 있어야 같은 유저임을 확인할 수 있다.

login.jsp

<div class="duplicate-login-alert">
	<c:if test="${DUPLICATE_LOGIN eq 'true'}">													
		<script> alert("다른 기기에서 로그인되어 현재 로그인이 종료되었습니다.") </script>											
	</c:if>
</div>
  • 기존 로그인 사용자 alert 알림 처리
    • 로그인 상태에서 새로운 세션으로 로그인을 시도하면, 기존 세션은 CustomSessionExpiredStrategy 에서 정의한 대로 request에 DUPLICATE_LOGIN = true 속성 값을 갖게 된다.
    • login.jsp 에서 request 속성 값에 DUPLICATE_LOGIN = true 가 있다면, alert 창을 띄우고, login 페이지를 띄워준다.

결과

  • 동일 아이디로 엣지 브라우저에서 로그인 후, 크롬 브라우저에 로그인 하면, 엣지 브라우저에서 새로고침, 메뉴 이동 등 동작 시 로그아웃 되면서 알림 창이 표시된다.

참고

spring boot login 중복 로그인 방지

spring security 중복로그인 방지 처리 안될때.

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

Spring 중복로그인 방지

profile
🔥

0개의 댓글