Session
📌 클라이언트와 서버 간의 상태를 유지하기 위해 서버가 클라이언트별로 생성하고 관리하는 데이터입니다.
- 결국 보안 문제를 해결하려면 중요한 정보는 모두 서버에서 저장해야한다. Client와 서버는 예측이 불가능한 임의의 값으로 연결해야 한다.
- 서버에서 중요한 정보를 보관하며 로그인 연결을 유지하는 방법을 Session 이라고 한다. 앞서 배운 Cookie는 중요한 정보를 Client측에서 보관하고 있는것이다.
Session 생성 순서
- 로그인에 성공하면 Server에서 임의로 만든 Session ID를 생성한다.
- Session ID는 예측이 불가능해야 한다.
- UUID와 같은 값을 활용한다.
- 생성된 Session ID와 조회한 User 인스턴스를 서버의 Session 저장소에 저장한다.
- 서버에 유저와 관련된 중요한 정보를 저장한다.
Session 동작 순서
로그인
- 상태유지를 위해 Cookie를 사용한다.
- 서버는 클라이언트에 Set-Cookie: SessionId=임의생성값 을 전달한다.
- 클라이언트는 Cookie 저장소에 전달받은 SessionId 값을 저장한다.
- Sessions을 사용하면 유저와 관련된 정보는 클라이언트에 없다.
로그인 이후 요청
- 클라이언트는 모든 요청에 Cookie 의 SessionId를 전달한다.
- 서버에서는 Cookie를 통해 전달된 SessionId로 Session 저장소를 조회한다.
- 로그인 시 저장하였던 Session 정보를 서버에서 사용한다.
Session 특징
- Session을 사용하여 서버에서 민감한 정보들을 저장한다.
- 예측이 불가능한 세션 ID를 사용하여 쿠키값을 변조해도 문제가 없다.
- 세션 ID에 중요한 정보는 들어있지 않다.
- 시간이 지나면 세션이 만료되도록 설정한다.
- 해킹이 의심되는 경우 해당 세션을 제거하면 된다.
- Session은 특별한것이 아니라 단지 Cookie를 사용하여 클라이언트가 아닌 서버에서 데이터를 저장해두는 방법이다.
- 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 을 생성하게되면 SessionId 가 JSESSIONID로 생성되고 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 정보
- session.getId();
- jsessionId 값을 조회할 수 있다.
- session.getMaxInactiveInterval();
- 세션의 유효시간
- second 단위 default는 30분(1800초)이다.
- session.getCreationTime();
- 세션 생성시간
- session.getLastAccessedTime();
- 해당 세션에 마지막으로 접근한 시간
- 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(수평적 확장)에서 서버간 세션 공유가 어렵다.