카테고리 없음

[Net] Session

JABHACK 2024. 12. 30. 21:04

Session

📌 클라이언트와 서버 간의 상태를 유지하기 위해 서버가 클라이언트별로 생성하고 관리하는 데이터입니다.

  • 결국 보안 문제를 해결하려면 중요한 정보는 모두 서버에서 저장해야한다. Client와 서버는 예측이 불가능한 임의의 값으로 연결해야 한다.
  • 서버에서 중요한 정보를 보관하며 로그인 연결을 유지하는 방법을 Session 이라고 한다. 앞서 배운 Cookie는 중요한 정보를 Client측에서 보관하고 있는것이다.

 

Session 생성 순서

 

  1. 로그인에 성공하면 Server에서 임의로 만든 Session ID를 생성한다.
    • Session ID는 예측이 불가능해야 한다.
    • UUID와 같은 값을 활용한다.
  2. 생성된 Session ID와 조회한 User 인스턴스를 서버의 Session 저장소에 저장한다.
    • 서버에 유저와 관련된 중요한 정보를 저장한다.

 

Session 동작 순서

로그인

  • 상태유지를 위해 Cookie를 사용한다.
    • 서버는 클라이언트에 Set-Cookie: SessionId=임의생성값 을 전달한다.
    • 클라이언트는 Cookie 저장소에 전달받은 SessionId 값을 저장한다.
  • Sessions을 사용하면 유저와 관련된 정보는 클라이언트에 없다.

 

로그인 이후 요청

  • 클라이언트는 모든 요청에 Cookie 의 SessionId를 전달한다.
  • 서버에서는 Cookie를 통해 전달된 SessionId로 Session 저장소를 조회한다.
  • 로그인 시 저장하였던 Session 정보를 서버에서 사용한다.

 

Session 특징

  1. Session을 사용하여 서버에서 민감한 정보들을 저장한다.
    • 예측이 불가능한 세션 ID를 사용하여 쿠키값을 변조해도 문제가 없다.
    • 세션 ID에 중요한 정보는 들어있지 않다.
    • 시간이 지나면 세션이 만료되도록 설정한다.
    • 해킹이 의심되는 경우 해당 세션을 제거하면 된다.
  2. Session은 특별한것이 아니라 단지 Cookie를 사용하여 클라이언트가 아닌 서버에서 데이터를 저장해두는 방법이다.
  3. Servlet은 Session 을 자체적으로 지원한다.

 

Servlet의 HttpSession

  • Servlet이 공식적으로 지원하는 Session인 HttpSession은 Session 구현에 필요한 다양한 기능들을 지원한다.

상수를 클래스로 관리하는 방법

// 추상클래스 -> O
public abstract class Const {
	public static final String LOGIN_USER = "loginUser";
}

// 인터페이스 -> O
public interface Const {
	String LOGIN_USER = "loginUser";
}

// 클래스 -> X
public class Const { // new Const();
	public static final String LOGIN_USER = "loginUser";
}

// Const.LOGIN_USER; O
// new Const(); X
  • 상수로 활용할 class는 인스턴스를 생성(new)하지 않는다.
  • abstract 추상클래스 혹은 interface 로 만들어 상수값만 사용하도록 만들어두면 된다.
Servlet을 통해 HttpSession 을 생성하게되면 SessionIdJSESSIONID로 생성되고 JSESSIONID의 Value는 예측 불가능한 랜덤값으로 생성된다.

=
Servlet 환경에서 HttpSession을 생성할 때 서버가 JSESSIONID라는 이름의 쿠키를 발급하고, 그 값이 랜덤하게 생성된 고유 식별자라는 뜻입니다. 이를 통해 세션을 식별하고 사용자와 서버 간의 상태를 유지합니다.
더보기

SessionUserController

@Controller
@RequiredArgsConstructor
public class SessionUserController {

    private final UserService userService;

    @PostMapping("/session-login")
    public String login(
            @Valid @ModelAttribute LoginRequestDto dto,
            HttpServletRequest request
    ) {

        LoginResponseDto responseDto = userService.login(dto.getUserName(), dto.getPassword());
        Long userId = responseDto.getId();

        // 실패시 예외처리
        if (userId == null) {
            return "session-login";
        }

        // 로그인 성공시 로직
        // Session의 Default Value는 true이다.
        // Session이 request에 존재하면 기존의 Session을 반환하고,
        // Session이 request에 없을 경우에 새로 Session을 생성한다.
        HttpSession session = request.getSession();

        // 회원 정보 조회
        UserResponseDto loginUser = userService.findById(userId);

        // Session에 로그인 회원 정보를 저장한다.
        session.setAttribute(Const.LOGIN_USER, loginUser);

        // 로그인 성공시 리다이렉트
        return "redirect:/session-home";
    }

    @PostMapping("/session-logout")
    public String logout(HttpServletRequest request) {
        // 로그인하지 않으면 HttpSession이 null로 반환된다.
        HttpSession session = request.getSession(false);
        // 세션이 존재하면 -> 로그인이 된 경우
        if(session != null) {
            session.invalidate(); // 해당 세션(데이터)을 삭제한다.
        }

        return "redirect:/session-home";
    }
}
  • request.getSession();
  • request.getSession(true);
    • Default 설정
    • Request 객체 내에 Session이 존재한다면 기존 Session을 반환
    • Request 객체 내에 Session이 없으면 새로운 Session을 생성해서 반환
  • request.getSession(false);
    • Request 객체 내에 Session이 존재한다면 기존 Session을 반환
    • Request 객체 내에 Session이 없으면 null을 반환
  • session.setAttribute(Const.LOGIN_USER, responseDto);
    • Session에 Data를 저장하는 방법으로 request.setAttribute(); 와 비슷하다.
    • 하나의 Session에 여러개의 데이터를 메모리에 저장할 수 있다.

 

SessionHomeController

요구사항1. 로그인한 회원이면 home 페이지로 이동한다.
요구사항2. home 페이지를 보려면 로그인이 필수이다.
요구사항3. 로그인하지 않은 회원이면 login 페이지로 이동한다.
@Controller
@RequiredArgsConstructor
public class SessionHomeController {

    private final UserService userService;

    @GetMapping("/session-home")
    public String home(
            HttpServletRequest request,
            Model model
    ) {

        // default인 true로 설정되면 로그인하지 않은 사람들도 값은 비어있지만 세션이 만들어진다.
        // session을 생성할 의도가 없다.
        HttpSession session = request.getSession(false);

        // session이 없으면 로그인 페이지로 이동
        if(session == null) {
            return "session-login";
        }

        // session에 저장된 유저정보 조회
        // 반환타입이 Object여서 Type Casting이 필요하다.
        UserResponseDto loginUser = (UserResponseDto) session.getAttribute(Const.LOGIN_USER);

        // Session에 유저 정보가 없으면 login 페이지 이동
        if (loginUser == null) {
            return "session-login";
        }

        // Session이 정상적으로 조회되면 로그인된것으로 간주
        model.addAttribute("loginUser", loginUser);
        // home 화면으로 이동
        return "session-home";

    }
}

Session은 Memory를 사용하기 때문에 리소스를 낭비하면 안된다.

 

session-home.html

<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
  <meta charset="UTF-8">
  <title>Home</title>
</head>
<body>
<h1>Welcome Session Home!</h1>

<p th:text="'안녕하세요, ' + ${loginUser.name} + '님!'">Hello, User!</p>

<!-- 로그아웃 버튼 -->
<form th:action="@{/session-logout}" method="post" style="margin-top: 10px;">
  <button type="submit">Logout</button>
</form>

</body>
</html>

 

session-login.html

<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
  <meta charset="UTF-8">
  <title>Session Login</title>
</head>
<body>
<h2>Session Login</h2>
<form th:action="@{/session-login}" method="post">
  <div>
    <label for="userName">Username:</label>
    <input type="text" id="userName" name="userName" required>
  </div>
  <div>
    <label for="password">Password:</label>
    <input type="password" id="password" name="password" required>
  </div>
  <button type="submit">Login</button>
</form>

</body>
</html>

 

 

브라우저 테스트

더보기

session-home 페이지 접근

로그인 실패

최초 로그인

이후 로그인

로그아웃

 

 

 

@SessionAttribute

📌 Spring에서는 Session을 쉽게 다루도록 @SessionAttribute라는 어노테이션이 제공된다.

  • request.getSession(true); 와는 다르게 Session을 새로 생성하는 기능은 없다.
  • 이미 로그인이 완료된 사용자를 찾는 경우 즉, Session이 있는 경우에 사용한다.
  • = 세션을 한번이라도 생성한 세션만 사용가능하다
@Controller
@Requiredargsconstructor
public class SessionHomeController {

		private UserService userService;
	
		@GetMapping("/v2/session-home")
    public String homeV2(
            // Session이 필수값은 아니다. 로그인 했거나 안했거나 판별해야하니 required false
            @SessionAttribute(name = Const.LOGIN_USER, required = false) UserResponseDto loginUser,
            Model model
    ) {

        // session에 loginUser가 없으면 Login 페이지로 이동
        if (loginUser == null) {
            return "session-login";
        }

        // Session이 정상적으로 조회되면 로그인된것으로 간주
        model.addAttribute("loginUser", loginUser);

        // home 화면으로 이동
        return "session-home";
    }
}

1. session-home 접속 및 로그인 이후 JSESSIONID 생성 및 Cookie 저장

2. /v2/session-home 에 접속해도 같은 결과를 확인할 수 있다.

  • 처음 로그인을 시도하는 경우
    • 완전히 처음 로그인을 시도하는 경우 URL 뒤에 JSSESSIONID=값이 함께 전달된다.
    • 해당 값은 굳이 필요가 없다. 내부적으로 Set-Cookie로 SessionID와 값을 넣어주기 때문
    • Cookie를 지원하지 않으면 URL을 통해 Session을 유지하는 방법에 사용된다.
      • 하지만, 사용하려면 모든 요청 URL에 jsessionId값이 전달 되어야한다.
Template Engine을 사용하면 jsessionId를 URL에 자동으로 포함한다.

 

URL이 아닌 Cookie를 통해서만 Session을 유지하고자 한다면?

= 클라이언트와 서버 간 세션 상태를 유지하기 위해 세션 ID를 쿠키에 저장하고 사용한다는 뜻입니다.
즉, URL에 세션 ID를 포함하지 않고 쿠키를 사용하여 세션을 식별하고 관리합니다.

 

application.properties

server.servlet.session.tracking-modes=cookie

application.yml

server: 
	servlet: 
		session: 
			tracking-modes: cookie

 

 

Session 정보

📌 HttpSession은 Session을 간편하게 사용할 수 있도록 다양한 기능을 지원한다.

@Slf4j
@RestController
public class SessionController {

    @GetMapping("/session")
    public String session(HttpServletRequest request) {
        HttpSession session = request.getSession(false);

        if (session == null) {
            return "세션이 없습니다.";
        }

        // session 정보 조회
        log.info("session.getId()={}", session.getId());
        log.info("session.getMaxInactiveInterval()={}", session.getMaxInactiveInterval());
        log.info("session.getCreationTime()={}", session.getCreationTime());
        log.info("session.getLastAccessedTime()={}", session.getLastAccessedTime());
        log.info("session.isNew()={}", session.isNew());

        return "세션 조회 성공!";
    }
    
}
  • Postman
    • 로그인

로그 출력

 

session 정보

  1. session.getId();
    • jsessionId 값을 조회할 수 있다.
  2. session.getMaxInactiveInterval();
    • 세션의 유효시간
    • second 단위 default는 30분(1800초)이다.
  3. session.getCreationTime();
    • 세션 생성시간
    ex) Sat Dec 9 15:40:23 KST 2024
  4. session.getLastAccessedTime();
    • 해당 세션에 마지막으로 접근한 시간
    ex) Sat Dec 9 15:40:23 KST 2024
  5. session.isNew();
    • 새로 생성된 세션인지 여부

 

Session TimeOut

📌 사용자가 일정 시간 동안 서버와 상호작용하지 않을 경우, 서버가 해당 세션을 자동으로 만료시키는 기능입니다. 이는 서버 리소스를 효율적으로 관리하고 보안을 강화하기 위해 사용됩니다.

  • Session은 logout 기능을 사용하여 session.invalidate(); 가 되어야 삭제되지만 대부분의 사용자들은 로그아웃을 굳이 하지않고, 브라우저를 종료한다.

 

Session의 문제점

  • HTTP는 Connectionless 특성을 가지고 있어서 서버가 브라우저 종료 여부를 판별하지 못한다.
  • 서버에서 Session을 언제 삭제해야 하는지 판단하기 힘들다.
  • JSESSIONID의 값을 탈취 당한 경우 해당 값으로 악의적인 요청을 할 수 있다.
  • 세션은 서버 메모리에 생성되고 자원은 한정적이기 때문에 꼭 필요한 경우만 생성해야 한다.

ex) 로그인한 유저가 100만명 이라면..?

 

Session 생명주기

  • 기본적으로 30분을 기준으로 세션을 삭제한다.
  • 실제 로그인 후 30분 이상의 시간동안 사용중인 사용자의 세션 또한 삭제된다.
  • 다시 로그인 해야하는 경우가 발생한다.

 

HttpSession 사용

  • 세션 생성시점 30분이 아닌 서버에 최근 Session을 요청한 시간을 기준으로 30분을 유지한다.
  • HttpSession은 기본적으로 해당 방식으로 세션의 생명주기를 관리한다.
  • Session 정보에서 LastAccessedTime 을 기준으로 30분이 지나면 WAS가 내부적으로 세션을 삭제한다.

 

 

Session의 한계

📌 Session은 서버의 메모리를 사용하여 확장성이 제한되어 있다.

  • 사용자가 많아질수록 서버가 관리해야 하는 세션 데이터가 증가하여, 서버의 메모리와 자원이 빠르게 소진됩니다.
오버헤드(Overhead)란? 어떤 처리를 하기 위해 들어가는 간접적인 처리 시간, 메모리 등을 의미한다.

 

Session의 한계

  • 서버가 DB 혹은 메모리에 저장된 세션 정보를 매번 조회하여 오버헤드가 발생한다.
  • 서버가 상태를 유지해야 하므로 사용자 수가 많아질수록 부담이 커진다.
  • Cookie는 웹 브라우저에만 존재하여 모바일 앱 등의 다양한 클라이언트에서 인증을 처리할 수 없다.
  • Scale Out(수평적 확장)에서 서버간 세션 공유가 어렵다.