오늘은 Session으로 로그인 기능 구현하는 부분에 대해서 정리하려고 한다.
기능 구현하는 걸 정리하기 전에 먼저 간단하게 Cookie와 Session이 무엇이고 어떤 차이점이 있는지부터 살펴보자
Cookie와 Session 이란?
HTTP 프로토콜의 특징은 클라이언트가 서버에 요청을 했을 때 요청에 맞는 응답을 보낸 후 연결을 끊는 처리방식이다.
연결(Connection)을 끊는 순간 클라이언트와 서버의 통신이 끝나며 상태 정보는 유지하지 않는 특성이 있다.(Stateless 프로토콜)
하지만, 개발을 하다보면 데이터 유지가 필요한 경우가 있는데 이런 무상태(Stateless) 경우를 대처하기 위해서 쿠키와 세션을 사용한다.
쿠키는 HTTP의 일종으로 사용자가 어떠한 웹 사이트를 방문할 경우, 그 사이트가 사용하고 있는 서버에서 사용자의 컴퓨터에 저장하는 작은 기록 정보 파일이다. HTTP에서 클라이언트의 상태 정보를 클라이언트의 PC에 저장하였다가 필요시 정보를 참조하거나 재사용할 수 있다.
세션은 일정 시간 동안 같은 사용자로부터 들어오는 일련의 요구를 하나의 상태로 보고, 그 상태를 유지시키는 기술이다. 여기서 일정 시간은 방문자가 웹 브라우저를 통해 웹 서버에 접속한 시점부터 웹 브라우저를 종료하여 연결을 끝내는 시점을 말하며, 접속해 있는 상태를 하나의 단위로 보고 그것을 세션이라고 한다.
쿠키와 세션의 큰 차이점은 상태 정보와 저장 위치인데, 쿠키는 클라이언트에 저장하고 세션은 서버에 저장한다.
쿠키는 클라이언트에 저장되기 때문에 보안에 취약하지만, 세션은 쿠키를 이용해서 session-id만 저장하고 그 값으로 구분하여 서버에서 처리하기 때문에 비교적 보안성이 높다.
로그인 시 Session의 동작 방식
- 사용자가 아이디와 비밀번호 정보를 전달하면 서버에서 해당 사용자가 맞는지 확인을 한다.
- 정보가 맞으면 세션 ID를 생성한다(이 값은 추정 불가능해야 한다)
- 생성된 세션 ID와 세션에 보관할 값을 서버의 세션 저장소에 보관한다.
- 서버는 클라이언트에게 세션 ID만 쿠키에 담아서 전달한다.
- 클라이언트는 쿠키 저장소에 전달 받은 쿠키를 보관한다.
- 클라이언트는 요청시 항상 전달 받은 쿠키를 전달한다.
- 서버에서는 클라이언트가 전달한 쿠키 정보로 세션 저장소를 조회해서 로그인시 보관한 세션 정보를 사용한다.
HttpSession으로 로그인 기능 구현하기
서블릿은 세션을 위해 HttpSession 이라는 기능을 제공한다.
서블릿을 통해 HttpSession을 생성하면 쿠키 이름이 'JSESSIONID'이고 값은 추정 불가능한 랜덤 값으로 생성이 된다.
SessionConst
HttpSession에 데이터를 보관하고 조회할 때 같은 이름이 중복 되어 사용되기 때문에 상수를 하나 정의해주자
public class SessionConst {
public static final String LOGIN_MEMBER = "loginMember";
}
LoginController
@Slf4j
@Controller
@RequiredArgsConstructor
public class LoginController {
private final LoginService loginService;
@PostMapping("/login")
public String login(@Validated @ModelAttribute("loginForm") LoginForm loginForm, BindingResult bindingResult, HttpServletRequest request) {
if (bindingResult.hasErrors()) {
return "login/loginForm";
}
Member loginMember = loginService.login(loginForm.getLoginId(), loginForm.getPassword());
log.info("login? {}", loginMember);
if (loginMember == null) {
bindingResult.reject("loginFail", "아이디 또는 비밀번호가 맞지 않습니다.");
return "login/loginForm";
}
// 세션이 있으면 존재하는 세션을 반환, 없으면 세션을 새롭게 생성
HttpSession session = request.getSession();
// 세션에 로그인 회원 정보 보관
session.setAttribute(SessionConst.LOGIN_MEMBER, loginMember);
return "redirect:/";
}
@PostMapping("/logout")
public String logout(HttpServletRequest request) {
HttpSession session = request.getSession(false);
if (session != null) session.invalidate();
return "redirect:/";
}
}
로그인 기능을 하는 부분을 살펴보자
- 세션을 생성하려면 request.getSession(true)를 사용하면 된다.
- 세션의 create 옵션은 기본적으로 true이다.
- true인 경우에는 세션이 있으면 기존 세션을 반환하고, 세션이없으면 새로운 세션을 생성해서 반환한다.
- false가 들어가 있는 경우 세션이 있으면 기존 세션을 반환하고, 세션이 없으면 새로운 세션을 생성하지 않고 null을 반환한다.
- session.setAttribute(SessionConst.LOGIN_MEMBER, loginMember); 을 통해서 세션에 로그인 회원 정보를 보관한다.
- 하나의 세션에 여러 값을 저장할 수 있다.
로그아웃 기능을 하는 부분을 살펴보자
- 세션을 가져올 때 세션이 없는 경우 새로운 세션을 생성하면 안되기 때문에 옵션을 false로 넣어준다.
- session.invalidate()를 통해서 세션을 제거한다.
HomeController
세션이 없거나 세션에 회원 데이터가 없으면 home으로 이동시키고 세션이 유지되면 loginHome으로 이동시킨다.
@Slf4j
@Controller
@RequiredArgsConstructor
public class HomeController {
private final MemberRepository memberRepository;
@GetMapping("/")
public String homeLogin(HttpServletRequest request, Model model) {
HttpSession session = request.getSession(false);
if (session == null) return "home";
Member loginMember = (Member) session.getAttribute(SessionConst.LOGIN_MEMBER);
if (loginMember == null) {
return "home";
}
model.addAttribute("member", loginMember);
return "loginHome";
}
}
home(thymeleaf)
<!DOCTYPE HTML>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="utf-8">
<link th:href="@{/css/bootstrap.min.css}"
href="css/bootstrap.min.css" rel="stylesheet">
</head>
<body>
<div class="container" style="max-width: 600px">
<div class="py-5 text-center">
<h2>홈 화면</h2>
</div>
<div class="row">
<div class="col">
<button class="w-100 btn btn-secondary btn-lg" type="button"
th:onclick="|location.href='@{/members/add}'|">
회원 가입
</button>
</div>
<div class="col">
<button class="w-100 btn btn-dark btn-lg"
onclick="location.href='items.html'"
th:onclick="|location.href='@{/login}'|" type="button">
로그인
</button>
</div>
</div>
<hr class="my-4">
</div>
</body>
</html>
loginHome(thymeleaf)
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="utf-8">
<link th:href="@{/css/bootstrap.min.css}"
href="../css/bootstrap.min.css" rel="stylesheet">
</head>
<body>
<div class="container" style="max-width: 600px">
<div class="py-5 text-center">
<h2>홈 화면</h2>
</div>
<h4 class="mb-3" th:text="|로그인: ${member.name}|">로그인 사용자 이름</h4>
<hr class="my-4">
<div class="row">
<div class="col">
<button class="w-100 btn btn-secondary btn-lg" type="button"
th:onclick="|location.href='@{/items}'|">
상품 관리
</button>
</div>
<div class="col">
<form th:action="@{/logout}" method="post">
<button class="w-100 btn btn-dark btn-lg" type="submit">
로그아웃
</button>
</form>
</div>
</div>
<hr class="my-4">
</div>
</body>
</html>
실제로 실행을 시켜서 확인을 해보면 아래와 같이 결과를 확인해 볼 수 있다.
로그인 전
로그인 후
@SessionAttribute을 사용하여 이미 로그인 된 사용자를 더 편하게 찾기
스프링은 세션을 더 편리하게 사용할 수 있도록 @SessionAttribute 를 지원한다.
해당 기능은 세션을 생성하지 않는 다는 점을 참고하자
기존의 HomeController에서 구현했던 코드에 아래와 같은 코드만 추가해주면 된다.
@SessionAttribute(name = SessionConst.LOGIN_MEMBER, required = false) Member loginMember
추가를 한 코드는 아래와 같다.
@GetMapping("/")
public String homeLogin(
@SessionAttribute(name = SessionConst.LOGIN_MEMBER, required = false) Member loginMember,
Model model) {
if (loginMember == null) return "home";
model.addAttribute("member", loginMember);
return "loginHome";
}
이렇게 하면 세션을 찾고, 세션에 들어있는 데이터를 찾는 과정을 직접 코드를 구현하지 않아도 스프링이 한번에 편리하게 처리해준다.
TrackingModes
로그인을 처음 시도하면 URL이 다음과 같이 jessionid를 포함하고 있는 것을 볼 수 있는데
이것은 웹 브라우저가 쿠키를 지원하지 않을 때 쿠키 대신 URL을 통해서 세션을 유지하는 방법이다.
URL 전달 방식을 끄고 항상 쿠키를 통해서만 세션을 유지하고 싶으면
application.properties나 yml 파일에 아래와 같은 옵션을 넣어주면 된다.
server.servlet.session.tracking-mode=cookie
만약 URL에 jsessionid가 꼭 필요하다면 다음 옵션을 추가해주자
spring.mvc.pathmatch.matching-strategy=ant_path_matcher
추가하지 않으면 jsessionid가 URL에 있을 때 404 오류가 발생할 수 있다.
세션 정보 확인
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpSession;
import java.util.Date;
@Slf4j
@RestController
public class SessionInfoController {
/**
* 세션이 제공하는 정보들을 확인
*/
@GetMapping("/session-info")
public String sessionInfo(HttpServletRequest request) {
HttpSession session = request.getSession(false);
if (session == null) return "세션이 없습니다.";
// 보관한 세션 데이터 출력
session.getAttributeNames().asIterator()
.forEachRemaining(name -> log.info("session name={}, value={}", name, session.getAttribute(name)));
log.info("sessionId={}", session.getId());
log.info("maxInactiveInterval={}", session.getMaxInactiveInterval());
log.info("creationTime={}", new Date(session.getCreationTime()));
log.info("lastAccessedTime={}", new Date(session.getLastAccessedTime()));
log.info("isNew={}", session.isNew());
return "세션 출력";
}
}
- sessionId : 세션Id, JSESSIONID 의 값이다.
- ex) 34B14F008AA3527C9F8ED620EFD7A4E1
- maxInactiveInterval : 세션의 유효 시간
- ex) 1800초, (30분)
- creationTime : 세션 생성일시
- lastAccessedTime : 세션과 연결된 사용자가 최근에 서버에 접근한 시간, 클라이언트에서 서버로 sessionId (JSESSIONID)를 요청한 경우에 갱신된다.
- isNew : 새로 생성된 세션인지, 아니면 과거에 이미 만들어졌고, 클라이언트에서 서버로 sessionId (JSESSIONID)를 요청해서 조회된 세션인지 여부 값
세션 타임아웃 설정하기
위에서 구현한 코드를 보면 사용자가 로그아웃 버튼을 클릭했을 때 session.invalidate()가 호출이 되어 세션이 삭제된다.
현실은 사용자가 로그아웃 버튼을 클릭하지 않고 그냥 웹 브라우저를 종료하는 경우가 대부분이다.
처음에 간략하게 세션에 대해서 설명했던 것처럼 HTTP가 비 연결성이므로 서버 입장에서는 해당 사용자가 웹 브라우저를 종료한 것인지 아닌지를 인식할 수 없기 때문에 서버에서 세션 데이터를 언제 삭제해야 하는지 판단하기가 어렵다.
이런 경우에는 남아있는 세션을 무한정 보관하면 세션과 관련된 쿠키를 탈취 당했을 경우 오랜 시간이 지나도 해당 쿠키로 악의적인 요청을 할 수 있고, 세션은 기본적으로 메모리에 생성되기 때문에 극단적으로 100만명의 사용자가 로그인하면 100만개의 세션이 생성이 되어버린다.
그렇다면 직접 세션의 종료시점을 설정해 주어야 하는데 세션의 종료 시점은 언제로 하는 것이 가장 좋을까 고민해보면 사용자가 서버에 최근 요청한 시간을 기준으로 n분 정도를 유지해주는 것이 좋다.
시간에 대한 부분은 기본이 30분이라는 것을 기준으로 고민하되 보관한 데이터 용량과 사용자 수로 세션의 메모리 사용량이 급격하게 늘어나면 장애로 이어질 수 있으니 적당한 시간을 선택하는 것이 필요하다.
스프링 부트로 세션 타임아웃을 글로벌 설정을 하려면 application.properties에 아래와 같이 설정을 해주면 된다.
server.servlet.session.timeout=60
기본은 1800초(30분)이고, 여기서는 테스트를 하기 위해서 1분으로 설정해주었다.
(글로벌 설정할 때는 분 단위로 설정해야한다)
특정 세션 단위로 시간 설정을 하려면 아래와 같이 직접 설정해주면 된다.
session.setMaxInactiveInterval(300);
300초로 5분 동안 세션을 유지해주는 설정이 된다.
세션의 타임아웃 시간은 해당 세션과 관련된 JSESSIONID를 전달하는 HTTP 요청이 있으면 현재 시간으로 다시 초기화가 된다.
이렇게 초기화되면 세션 타임아웃으로 설정한 시간동안 세션을 추가로 사용할 수 있다.
서버에 최근 요청한 시간을 기준으로 설정한 시간이 지나면 WAS가 내부에서 해당 세션을 제거한다.
'TIL > Spring' 카테고리의 다른 글
[Spring] ArgumentResolver (HandlerMethodArgumentResolver)란 무엇일까? (0) | 2025.03.29 |
---|---|
[Spring] 서블릿 필터와 스프링 인터셉터(Servlet Filter, Spring Interceptor) (0) | 2025.03.28 |
[Spring] Spring - Bean Validation (0) | 2025.03.20 |
[Spring] Spring Validation-BindingResult(with.thymeleaf) (0) | 2025.03.16 |
[Spring] 빈 스코프 란? (0) | 2025.03.08 |