웹을 하다보면 자주 접하는 중복 로그인 방지 기능 요청사항이 들어오는데, HttpSessionListener를 통해 관리할 수 있습니다. 해당 객체는 Session이 생성되거나 제거될때 발생하는 이벤트를 제공하므로 등록만 해주면 세션을 통제할 수 있습니다.
HttpSessionListener
@WebListener
public class SessionConfig implements HttpSessionListener {
@Override
public void sessionCreated(HttpSessionEvent hse) {
//세션 생성시
}
@Override
public void sessionDestroyed(HttpSessionEvent hse) {
//세션 삭제시
}
}
HttpSessionListener는 EventListener를 상속받아 구현되어 있고 sessionCreated와 sessionDestroyed를 상속받습니다.
메소드명 그대로 sessionCreated는 세션이 생성될 때 동작하며, sessionDestroyed는 세션이 삭제될 때 생성됩니다.
우리는 해당 리스너에서 전역으로 처리할 세션 컬렉션을 통해 관리를 할 예정입니다.
private static final Map<String, HttpSession> sessions = new ConcurrentHashMap<>();
ConcurrentHashMap을 사용하여 세션을 처리합니다.
ConcurrentHashMap는 일반 HashMap과는 다르게 key, value값으로 Null을 허용하지 않는 컬렉션입니다.
다음은 해당 리스너를 사용한 예제입니다.
사용 예제
SessionConfig.java
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import javax.servlet.annotation.WebListener;
import javax.servlet.http.HttpSession;
import javax.servlet.http.HttpSessionEvent;
import javax.servlet.http.HttpSessionListener;
@WebListener
public class SessionConfig implements HttpSessionListener {
private static final Map<String, HttpSession> sessions = new ConcurrentHashMap<>();
//중복로그인 지우기
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);
}
}
@Override
public void sessionCreated(HttpSessionEvent se) {
System.out.println(se);
sessions.put(se.getSession().getId(), se.getSession());
}
@Override
public void sessionDestroyed(HttpSessionEvent se) {
if(sessions.get(se.getSession().getId()) != null){
sessions.get(se.getSession().getId()).invalidate();
sessions.remove(se.getSession().getId());
}
}
}
SessionConfig는 @WebListener 어노테이션을 통해 리스너임을 명명합니다.
어노테이션 방식이 아닌 설정파일에 추가할 경우 web.xml에 listener태그를 통해 추가해주시면 됩니다.
@WebListener가 아닌 web.xml설정하기
<listener>
<listener-class>패키지경로.SessionConfig</listener-class>
</listener>
getSessionCheck메소드에 synchronized키워드가 보이실 텐데, 모르는분들을 위하여 간단하게 설명하면 멀티쓰레드로 인한 동시 접근을 막아 처리의 순서를 보장하기위해 해당 키워드를 처리하였습니다. (톰캣은 접속하는 세션이 늘어날 때마다 쓰레드가 증가됩니다. 기본 개수는 200개였던것으로 기억합니다...)
login.jsp
<%@ page language="java" contentType="text/html; charset=UTF-8"
pageEncoding="UTF-8"%>
<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<title>Session Test</title>
</head>
<body>
<form action="/login.do" method="POST">
id : <input type="text" id="id" name="id">
<input type="submit" value="login">
</form>
</body>
</html>
간단하게 ID만 받을 form태그입니다.
LoginController.java
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpSession;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.servlet.mvc.support.RedirectAttributes;
@Controller
public class LoginController {
@RequestMapping(value = "/login.do", method = RequestMethod.POST)
public String login(HttpServletRequest request, HttpSession session, RedirectAttributes rttr) throws Exception {
String id = request.getParameter("id");
if(id != null){
String userId = SessionConfig.getSessionidCheck("login_id", id);
System.out.println(id + " : " +userId);
session.setMaxInactiveInterval(60 * 60);
session.setAttribute("login_id", id);
return "redirect:/home.do";
}
return "redirect:/main.do";
}
@RequestMapping(value = "/main.do")
public String index(HttpSession session) throws Exception {
return "login";
}
@RequestMapping(value = "/home.do")
public String home(HttpSession session) throws Exception {
return "home";
}
}
메인 페이지, 로그인 성공시 home.jsp로 처리할 매핑 함수와 로그인 처리를 할 함수를 구성한 로그인 컨트롤러입니다.
마지막으로 인터셉터를 구성 후 테스트를 진행해보겠습니다.
LoginInterceptor.java
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
import org.springframework.web.servlet.handler.HandlerInterceptorAdapter;
public class LoginInterceptor extends HandlerInterceptorAdapter {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
final HttpSession session = request.getSession();
String path = request.getRequestURI();
if(path.contains("/main.do") || path.contains("/login.do")) { //접근 경로가 main.do인 경우에인 interceptor 체크 예외
return true;
}else if (session.getAttribute("login_id") == null) { //세션 로그인이 없으면 리다이렉트 처리
response.sendRedirect("/main.do");
return false;
}
return true;
}
}
main.do, login.do를 제외하고 세션의 login_id값이 존재하는지 체크 후 존재하지 않는다면 기본 페이지로 리다이렉트 처리를 해주는 인터셉터입니다.
인터셉터 사용을 위해 설정을 추가합니다.
servlet-context.xml
<mvc:interceptors>
<mvc:interceptor>
<mvc:mapping path="/*.do" />
<beans:bean class="com.test.httpSessions.LoginInterceptor" />
</mvc:interceptor>
</mvc:interceptors>
결과 테스트
테스트를 위해 파이어폭스에서 main.do를 접근하였습니다.
다음은 IE에서 추가로 접근하였습니다.
해당 서버에 접근하는 순간 세션이 생기면서 SessionConfig.sessionCreated에 sysout을 찍어놓은 부분이 동작하여 주소값이 출력되는 모습을 확인할 수 있습니다.
이제 로그인 테스트를 해보겠습니다. DB는 제외되었고 당연히 정상 입력이라고 가정하고 테스트를 진행합니다.
다음은 IE에서 admin으로 로그인해보겠습니다.
역시 정상적으로 접속 되었고 파이어폭스에서 새로고침을 해보면 로그인 페이지로 이동된 것을 볼 수 있습니다.
기존에 접속되어있던 admin 중복 세션을 제거하였기 때문입니다.
/home.do는 세션을 체크하는 인터셉터에 의하여 초기페이지로 이동된 모습을 확인 할 수있습니다.
짤버전 추가...
* 세션 중복제거를 하면서 헷갈렸던 부분은 SessionConfig의 sessionCreated메소드였는데, 당연히 session.setAttribute의 동작이 이루어지면 리스너에 의해서 이부분이 매번 동작되는것으로 착각하여 초기에 많은 시간의 뻘짓을 하였습니다. 죄 없는 이클립스가 캐시가 먹었다부터 해서 브라우저가 고장난건지 여러 의문을 품었지만 역시 컴퓨터는 거짓말을 하지 않습니다... 세션이 클라이언트가 해당 서버에 접근하는 순간 생성되는것이며, login_id의 관리는 별개로 컬렉션에서 관리하는것이므로 두 개념을 헷갈려서는 안됩니다...
'WEB > Spring' 카테고리의 다른 글
Spring - java.lang.IllegalArgumentException: No converter found for return value of type: class java.util.ArrayList (0) | 2020.04.23 |
---|---|
Spring - Filter에서 @PostConstruct 처리시 2번 이상 동작하는 현상 (0) | 2020.04.14 |
Spring - lombok @AllArgsConstrutor 인식 에러, 동작 안함 (0) | 2020.04.08 |
Spring WebFlux - 몽고 DB 연동하기 (0) | 2020.04.06 |
Spring WebFlux - ajax(비동기)를 통한 데이터 파싱 예제 (0) | 2020.03.25 |