로그인의 상태를 어떻게 유지할 수 있을까? 쿼리 파라미터를 계속 유지하면서 보내는 것은 매우 어렵고 번거로운 작업이다. 쿠키를 사용해보자!
서버에서 로그인에 성공하면 HTTP 응답에 쿠키를 담아서 브라우저에 전달한다.
그러면 브라우저는 앞으로 해당 쿠키를 지속해서 보내준다.
@PostMapping("/login")
public String login(@Valid @ModelAttribute LoginDTO loginDTO, BindingResult bindingResult,
HttpServletResponse response) {
if (bindingResult.hasErrors()) {
return "admin/loginForm";
}
AdminDTO findAdmin = loginService.loginAdmin(loginDTO.getId(), loginDTO.getPassword());
log.info("findAdmin = {}", findAdmin);
// 로그인이 실패한 경우 alert 후 다시 로그인 페이지 제공
if (findAdmin == null) {
bindingResult.reject("loginFail", "아이디 또는 비밀번호가 맞지 않습니다.");
return "admin/loginForm";
}
// 로그인 성공 처리
Cookie idCookie = new Cookie("adminId", loginDTO.getId());
response.addCookie(idCookie);
return "redirect:/";
}
Cookie idCookie = new Cookie("adminId", loginDTO.getId());
response.addCookie(idCookie);
로그인 성공 시, 1. 쿠키를 생성하고 2. HttpServletResponse
에 담는다. 웹브라우저는 종료 전까지 회원의 id를 서버에 계속 보내 줄것이다.
@GetMapping("/")
public String homeLogin(@CookieValue(name = "adminId", required = false) String adminId, Model model) {
if(adminId == null) {
log.info("adminId가 null입니다.");
log.info("adminID={}", adminId);
return "home";
}
// 쿠키와 일치하는 id가 없는 경우, 다시 비로그인 홈으로 이동합니다.
AdminDTO loginAdmin = adminService.findAdminByAdminId(adminId);
if(loginAdmin == null) {
log.info("조회한 loginAdmin이 null 입니다");
return "home";
}
model.addAttribute("admin", loginAdmin);
log.info("loginHome으로 이동");
return "loginHome";
}
@CookieValue
로 편리하게 쿠키를 조회할 수 있다.required = false
를 사용한다. Cookie cookie = new Cookie(cookieName, null);
cookie.setMaxAge(0);
response.addCookie(cookie);
로그아웃은 응답 쿠키를 생성하면서 Max-Age=0
를 설정하여 수행한다. 해당 쿠키는 즉시 종료된다.
😥사용자를 식별하기 위해 쿠키에 인식 요소를 사용하는건데, 쿠키에 중요한 정보를 보관하면 안되겠네...
🤔 그럼 1.중요한 정보는 모두 서버에 저장해야 하고, 2.서버에서 식별에 이용되는 임의의 식별값이 추정 불가능해야 겠구나!
이렇게 서버에 중요한 정보를 보관하고 연결을 유지하는 방법을 세션이라고 한다.
Cookie: mySessionId=zz0101xx-bab9-4b92-9b32-dadb280f4b61
mySessionId
라는 이름으로 세션 ID만 쿠키에 담아서 전달한다.mySessionId
쿠키를 보관한다.mySessionId
쿠키를 전달한다.mySessionId
쿠키 정보로 세션 정보를 조회해서 로그인시 보관한 세션 정보를 사용한다.세션을 이용해 이제는 서버에서 중요한 정보를 관리한다. 따라서 다음과 같은 문제도 해결 가능하다.
//TODO 추가
서블릿은 세션을 위해 HttpSession
이라는 기능을 제공하고 있다.
서블릿을 통해 HttpSession을 생성하면 다음과 같은 쿠키를 생성한다. 이름은 JESSIONID이고, 값은 추정 불가능한 랜덤 값이다.
Cookie: JSESSIONID=5B78E23B513F50164D6FDD8C97B0AD05
//세션이 있으면 있는 세션 반환, 없으면 신규 세션 생성
HttpSession session = request.getSession();
//세션에 로그인 회원 정보 보관
session.setAttribute(SessionConst.LOGIN_MEMBER, loginMember);
return "redirect:/";
public HttpSession getSession(boolean create);
request.getSession(true)
(기본값)request.getSession(false)
session.setAttribute(SessionConst.LOGIN_MEMBER, loginMember);
세션에 데이터 보관하는 방법은 request.setAttribute(..)
와 비슷하다. 하나의 세션에 여러 값을 저장할 수 있다.
@GetMapping("/")
public String homeLoginV3(HttpServletRequest request, Model model) {
//세션이 없으면 home (세션 자체가 없는 경우)
HttpSession session = request.getSession(false);
if (session == null) {
return "home";
}
Member loginMember = (Member) session.getAttribute(SessionConst.LOGIN_MEMBER);
//세션에 회원 데이터가 없으면 home (세션은 있는데 세션과 일치하는 회원이 없는 경우)
if (loginMember == null) {
return "home";
}
//세션이 유지되면 로그인으로 이동 (세션이 있고, 그와 일치하는 회원도 있는 경우 => 로그인 진행)
model.addAttribute("member", loginMember);
return "loginHome";
}
request.getSession(false)
request.getSession()
를 사용하면 기본 값이 create: true
이므로, 로그인 하지 않을 사용자도 의미없는 세션이 만들어진다. create: false
옵션을 사용해서 세션을 생성하지 않아야 한다.session.getAttribute(SessionConst.LOGIN_MEMBER)
😣아.. 이거 session에 값 있는지 확인하고, 또 꺼내서 비교하고, 이런거 귀찮은데 더 간단한 방법이 없나?
😉 그럴때, @SessionAttribute를 사용할 수 있습니다!
스프링이 제공하는 세션 기능
@SessionAttribute(name = "loginMember", required = false) Member loginMember
@GetMapping("/")
public String homeLogin(@SessionAttribute(name = SessionConst.LOGIN_ADMIN, required = false) AdminDTO loginAdmin,
Model model) {
// 세션에 회원 데이터가 없으면 home으로 이동
if (loginAdmin == null) {
return "home";
}
// 세션이 유지되면 loginHome으로 이동
model.addAttribute("admin", loginAdmin);
return "loginHome";
}
세션을 찾고, 세션에 들어있는 데이터를 찾는 번거로운 과정을 스프링이 한번에 편리하게 처래히준다.
로그인을 처음 시도하면 urL이 jessionid
를 포함하고 있다.
http://localhost:8080/;jsessionid=F59911518B921DF62D09F0DF8F83F872
이는 웹 브라우저가 쿠키를 지원하지 않을때, 쿠키 대신 URL을 통해 세션을 유지하라는 용도로 서버에서 이 값을 제공한다.(이 방법을 위해선 url에 계속 이 값을 포함해서 전달해야 한다) 서버입장에서는 웹 브라우저가 쿠키를 지원하는지 하지 않는지 최초에는 판단하지 못하므로, 쿠키 값도 전달하고, URL에 jessionid
도 함게 전달하는 것이다.
URL 전달 방식을 끄고 항상 쿠키를 통해서면 세션을 유지하고 싶다면, 다음 옵션을 넣어주면 된다.
application.properties
server.servlet.session.tracking-modes=cookie
package com.study.admin.controller;
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, JESSIONID의 값. 34B14F008AA3527C9F8ED620EFD7A4E1
maxInactiveInterval
: 세션의 유효 시간createTime
: 세션 생성 일시lastAccessedTime
: 세션과 연결된 사용자가 최근에 서버에 접근한 시간, 클라이언트에서 서버로 sessionID
를 요청한 경우에 갱신된다.isNew
: 새로 생성된 세션인지, 아니면 이미 과거에 만들어졌고, 클라이언트에서 서버로 sessionId를 요청해서 조회된 세션인지 여부session.invalidate()
가 호출되는 경우에만 삭제된다.만약 세션이 무한정 보관된다면 어떤 문제점들이 발생할까?
🤔그럼 세션 종료 시점을 언제로 정하면 좋을까?
한 30분 정도 유지하는것이 좋을것 같다.
HttpSession
은 이 방식을 사용하고 있다.server.servlet.session.timeout=60
기본은 1800(30분)
최소값 60
session.setMaxInactiveInterval(1800); //1800초
세션의 타임아웃 시간은 해당 세션과 관련된 JESSIONID를 전달하는 HTTP 요청이 있으면 현재 시간으로 다시 초기화된다. 초기화 되면, 타임아웃으로 설정한 시간만큼 세션을 추가로 사용할 수 있다.
session.getLastAccessedTime()
: 최근 세션 접근 시간
LastAccessedTime
이후로 timeout시간이 지나면, WAS가 내부에서 해당 세션을 제거한다.
세션에는 최소한의 데이터만 보관해야 한다. 이를 주의하지 않으면 (보관한 데이터 용량 * 사용자 수)로 세션의 메모리 사용량이 급격히 늘어나 장애로 이어질 수 있다.
또한 세션의 시간을 너무 길게 가져가면 메모리 사용이 계속 누적될 수 있으므로 적당한 시간을 선택하는 것이 필요하다. (기본은 30분)