Spring session 브라우저 종료 - Spring session beulaujeo jonglyo

http://java.sun.com/j2ee/sdk_1.3/techdocs/api/javax/servlet/http/package-summary.html

이곳에 보시면, 서블릿 컨텍스나 세션의 변화를 체크하여 리스너들이 있습니다.

리스너 인터페이스를 구현하여 클래스를 만들면 되구요 web.xml에 해당 리스너 클래스를 추가해 주시면 됩니다.

package com.cafe24.youmasan.common;

import java.sql.SQLException;

import javax.servlet.http.HttpSession;
import javax.servlet.http.HttpSessionEvent;
import javax.servlet.http.HttpSessionListener;

public class LoggingListener
implements HttpSessionListener {

 public LoggingListener() {
 }

 public void sessionCreated(HttpSessionEvent hse) {
  HttpSession session = hse.getSession();
  session.setMaxInactiveInterval(60*30);//초단위로 세션유지 시간을 설정합니다
  System.out.println(session.getId()+": 세션이 생성되었습니다.");
 }


 public void sessionDestroyed(HttpSessionEvent hse) {

  HttpSession session = hse.getSession();
  System.out.println(session.getId()+": 세션이 소멸되었습니다."); 
 }

}

세션의 변화를 체크하기 위해 HttpSessionListener인터페이스를 구현해 리스너 클래스를 만들구요

web.xml <web-app>태그안에

   </servlet>
 <listener>
  <listener-class>com.cafe24.youmasan.common.LoggingListener</listener-class>
 </listener>

리스너를 등록해 주시면 됩니다.

중간에

 session.setMaxInactiveInterval(60*30);//초단위로 세션유지 시간을 설정합니다

설정을 통해 세션의 생명시간을 설정해 줄수 있습니다.

브라우저가 닫힐경우 session.invalidate()같은 세션종료 명령을 서버에 보내주지 못합니다.

그렇게 때문에 서버는 브라우저와 세션의 연결이 끊겼다는 사실을 모르고 마냥 요청을 기다리게 됩니다.

세션의 생명시간동안 응답이 없다면  그제서야 서버는 닫힌 브라우저와의 세션을 종료합니다.

인터페이스를 구현한

 public void sessionDestroyed(HttpSessionEvent hse) {}

메소드안에서 세션종료시 처리 로직을 추가해 주시면 됩니다.

출처 - http://blog.daum.net/nationisone/8197009

웹 어플리케이션에서 사용자의 사용성을 증진시키기 위해서는 로그인이 필수이다.
로그인 기능이 존재한다면 사용자의 정보도 일정 시간동안 유지시켜야 하는데 웹은 HTTP 프로토콜로 통신하는 만큼 상태를 유지시키지 않는다.
그렇다면 어떻게 사용자의 정보를 유지시킬 수 있을까?


쿠키만 사용하여 유저의 정보를 유지하는 방법

서버와 클라이언트의 대화

클라이언트 : 로그인 할래 
서버 : OK, Cookie 저장소에 Member 키값 저장해. 다음부터 요청할 때 포함해서 전송해줘. 

클라이언트 : GET, Cookie 포함, 정보 보여줘 
서버 : OK, 유저 정보 확인, 정보 보여줄게. 

클라이언트 : 브라우저 종료, 만료 날짜가 없네? 세션쿠키니까 삭제. 
                      or 만료 날짜가 있네 삭제 안해! 만료 날짜일 때 삭제

쿠키 구현하기 Response.setCookie()

첫 로그인 성공

Cookie cookie = new Cookie("memberId", String.valueOf(getId()));
response.setCookie(cookie);
return "redirect:/";

로그인 로직

public String login(@CookieValue(name = "memberId"), required = false) Long memberId, Model model) {
    if (memberId == null) {
    	// 쿠키가 없으므로 로그인 창으로 이동  
    }
    
    // 쿠키 확인, 쿠키 값으로 사용자 정보 가져오기
    Member member = memberRepository.findById(memberId);
    model.addAtribute("member", member); 
}

로그아웃 로직

cookie.setMaxAge(0);
response.setCookie(cookie);

쿠키의 심각한 보안문제

쿠키에는 굉장히 심각한 보안문제가 존재한다. 첫번째로 단순히 서버에 전송하는 쿠키를 조작하여 서버에 옳지 않은 요청을 시도할 수도 있다.
두번째로 정보가 브라우저에도 저장되기 때문에 Local PC가 털릴 경우 쿠키도 동시에 털릴 확률이 굉장히 높으며, 브라우저에서 서버로 쿠키 정보를 전송하는 과정에 털릴 확률이 높다. 또한 이를 통해 쿠키를 탈취 하면 해커는 서버를 통해 악의적인 요청을 계속 시도하여 부가적인 피해를 입히게 된다.

위의 문제들을 손쉽게 해결하기 위해 보통은 쿠키와 세션을 같이 사용한다. 한번 자세히 알아보도록 하자.

참고) 토큰이란 무엇일까? 세션과 차이점은 무엇이지?
클라이언트 : SessionID나 토큰을 포함하여 요청을 전송한다는 점에서는 유사하다.
서버 : 세션을 활용하는 경우에는 세션 저장소를 별도로 운영하고, 토큰을 활용하는 경우에는 토큰을 암호화, 복호화하는 책임을 가지고 있다는 것에서 차이가 존재한다.


쿠키와 세션을 통해 유저의 정보를 유지하는 방법

쿠키의 보안문제를 해결하기 위해서는 결국 매핑될 임의의 값을 유저에게 전해주고 중요한 정보들은 모두 서버에서 관리해야 한다는 것을 알 수 있다.
이렇게 서버에 중요한 정보를 보관하고 연결을 유지하는 것을 세션이라고 한다.

서버와 클라이언트의 대화

클라이언트 : 로그인 할래 
서버 : OK 정보가 맞네, 내 세션 저장소에 니 정보를 저장 해놔야겠다. 나는 너에게 정보를 찾을 수 있는 Key인 sessionID를 줄게 쿠키 저장소에 저장해.

클라이언트 : GET, Cookie 포함, 정보 보여줘
서버 : OK, sessionId에 매핑된 정보가 존재하네, 정보 보여줄게

위와 같이 세션을 사용함으로서 해결되는 문제는 다음과 같다.
1. sessionID를 해커가 예상 불가능한 값으로 임의로 생성한다.
2. 중요한 정보를 서버에만 저장하여 Local PC에서 탈취될 우려가 없다.
3. 세션 만료 시간을 짧게하여 sessionID를 털어가도 악의적인 행동을 취할 수 없도록 한다.

세션 구현하기

깊은 이해를 위해 직접 세션을 구현해보자. 좀 더 쉬운 이해를 위해 서버와 클라이언트의 대화와 로직을 매칭 시켜봤다.

클라이언트 : 로그인 할래
서버 : 내 세션 저장소에 니 정보를 저장 해놔야겠다. 나는 너에게 정보를 찾을 수 있는 Key인 sessionId를 줄게 쿠키 저장소에 저장해
public void createSession(Object value, HttpServletResponse response) {
    // sessionID 생성
    String sessionId = UUID.randomUUID().toString();

    // 세션 저장소에 sessionId와 value 저장
    sessionStore.put(sessionId, value);

    // sessionId로 응답 쿠키를 생성해서 클라이언트에 전달
    Cookie cookie = new Cookie(SESSION_COOKIE_NAME, sessionId);
    response.addCookie(cookie);
}

UUID.randomUUID()를 활용하게 되면 쉽게 UUID 랜덤키를 얻을 수 있다. 얻어낸 sessionId를 key로 하여 Map에 유저 정보를 저장한다. 과정을 마친 후 responsesessionId를 포함시켜 응답한다.

클라이언트 : Get, Cookie 포함, 정보 보여줘
서버 : OK, sessionId에 매핑된 정보가 존재하네, 정보 보여줄게
public Object getSession(HttpServletRequest request) {
    // 세션과 관련된 쿠키 가져오기
    Cookie cookie = findCookie(request, SESSION_COOKIE_NAME);
    if (cookie == null) return null;

    // 쿠키에 저장된 sessionId를 통해 세션을 가져온다
    return sessionStore.get(cookie.getValue());
}

public Cookie findCookie(HttpServletRequest request, String cookieName) {
    Cookie[] cookies =  request.getCookies();
    if (cookies == null) {
        return null;
    }

    return Arrays.stream(cookies)
            .filter(c -> c.getName().equals(cookieName))
            .findAny()
            .orElse(null);
}

우선 클라이언트에게 가져온 쿠키 목록으로부터 내가 필요로 하는 쿠키를 뽑아내야 한다. 그렇게 뽑아낸 쿠키로부터 sessionId를 얻어낸다. sessionId를 통해 Map에서 sessionId에 해당되는 유저 정보를 가져온다.

위와 같이 세션을 직접 일일히 구현하는 것은 상당히 불편하다. 개발자들은 불편한 것을 절대로 좋아하지 않는다. 따라서 서블릿도 세션 자체를 지원한다. 한번 알아보도록 하자.

서블릿이 제공하는 HttpSession

서블릿은 세션을 제공하기 위해 HttpSession을 제공한다. HttpSession은 직접 구현한 세션과 동일한 기능에 추가적으로 일정시간 사용하지 않으면 삭제되는 기능을 제공한다. 서블릿을 통해 HttpSession을 생성하면 JSESSIONID라는 이름을 가진 쿠키를 생성한다. 세션을 생성하는 방법을 알아보자.

loginController.java

//세션이 있으면 있는 세션 반환, 없으면 신규 세션을 생성
HttpSession session = request.getSession(/*create 옵션*/);
session.setAttribute(SessionConst.LOGIN_MEMBER, loginMember);

create 옵션
1. true (default) : 세션이 없으면 새로운 세션을 생성해서 반환
2. false : 세션이 없으면 새로운 세션을 생성하지 않고 null을 반환한다.

참고)
세션은 메모리를 사용하여 저정하기 때문에 최소한의 데이터만 저장하도록 하자

세션을 생성 했으니 세션을 사용하는 방법을 알아보자.

homeController.java

HttpSession session = request.getSession(false);
Member loginMember = (Member)session.getAttribute(SessionConst.LOGIN_MEMBER);

서블릿에서 제공하는 HttpSession을 통해 세션을 구현하는 방법을 알아봤다. 이정도 만으로도 충분하지만 Spring은 여기서 더 나아가서 좀 더 편리하게 세션 기능을 구현할 수 있도록 @SessionAttribute를 지원한다.

homeController.java

public String homeLoginV3Spring(
        @SessionAttribute(name = SessionConst.LOGIN_MEMBER, required = false) Member loginMember, Model model) {
    model.addAttribute("member", loginMember);
	  ...
}

TrackingModes

로그인을 처음 시도하면 URL 뒤에 JSESSIONID가 포함되어 있는 것을 볼 수 있다.

http://localhost:8080....../;jsessionid=...

이것은 웹 브라우저가 쿠키를 지원하지 않을 때, 쿠키 대신에 URL을 통해서 세션을 유지하는 방법이다. 이 방법을 통해 쿠키를 지원하기 위해서는 모든 URL 뒤에 JSESSIONID를 포함하여 전달해야 한다. 복잡한 방법이라 잘 사용하지 않는다. 서버 입장에서는 브라우저가 쿠키를 지원 하는지 안하는지 알지 못하기 떄문에 처음에 무작정 URL에 JSESSIONID를 포함하여 전달하는 것이다.

물론 위의 옵션을 아래와 같은 설정을 통해 없앨 수 있다.

application.proerties

server.servlet.session.tracking-modes=cookie

세션 타임아웃 설정

보통 로그아웃시에 session.invalidate()가 호출되어 세션이 삭제된다. 그런데 보통 사용자들은 로그아웃 버튼을 누르지 않고 브라우저 자체를 종료해버린다. HTTP는 stateless 프로토콜이기 때문에 서버 입장에서는 사용자가 브라우저를 종료한 것을 알지 못한다.

이렇게 되면 서버에서 세션을 메모리에 저장하여 관리하는데, 수천, 수만명의 세션이 계속 쌓이게 되어 서버가 터져 버릴수도 있다. 또한 무한정 남아있는 쿠키를 해커에게 탈취 당해 악의적인 요청을 당할 수 있다.

즉, 세션을 일정시간이 지나면 자동으로 삭제 되도록 처리해야만 한다.

참고)
세션은 정말 최소한의 데이터만 보관해야 한다. 유저수가 많아지게 되면 세션 때문에 메모리 용량을 벗어나게 되어 정말로 서버가 터질수도 있기 때문이다.

그렇다면 세션을 언제 종료시켜야 할까?

만약 간단하게 일정 시간마다 세션을 삭제시키면 계속 사용하던 사용자는 갑자기 로그아웃 되는 불편한 상황을 맞이하게 될 것이다. 그렇다면 어떤 시점에 종료해야 사용자가 불편함을 느끼지 않게끔 세션을 종료시킬 수 있을까?

사용자가 서버에 요청한 시간을 기준으로 시간을 계산하여 종료한다.
HttpSession은 기본적으로 위와 같은 방식을 사용하여 생명주기를 관리한다.

생명주기 설정법을 알아보자, 글로벌한 설정법과 세션마다 설정할 수 있는 설정법이 있는데 우선 글로벌 설정법 부터 알아보자.

application.properties

server.servlet.session.timeout=1800 (1800초) 

다음은 특정 세션만 지정하는 경우이다.

session.setMaxInactiveInterval(1800);

1800초로 지정하게 되면 최근 세션 접근 시간 (LastAccessedTime) 이후로 1800초 시간이 지나면, WAS가 세션을 제거한다. 만약에 1800초 전에 재 요청을 하게 되면 그 시점부터 다시 1800초를 계산하게 된다.

최근 세션 접근 시간 = session.getLastAccessedTime()

출처
Inflearn 스프링 MVC 2편 - 백엔드 웹 개발 활용 기술 - 김영한님 강의를 수강하며 정리한 내용입니다