내가 공부한것을 올리며, 중요한 단원은 저 자신도 곱씹어 볼겸 상세히 기록하고 얕은부분들은 가겹게 포스팅 하겠습니다.
7. 로그인 처리하기 - 직접 만든 세션 적용
지난 시간까지 직접 구현한 SessioManager를 활용하여 로그인, 로그아웃을 구현해보자!
● LoginController - loginV2()
@Slf4j
@Controller
@RequiredArgsConstructor
public class LoginController {
private final LoginService loginService;
private final SessionManager sessionManager; // 세션 메니저 추가
@GetMapping("/login")
public String loginForm(@ModelAttribute LoginForm form){
return "login/loginForm";
}
@PostMapping("/login")
public String loginV2(@Validated @ModelAttribute LoginForm form, BindingResult bindingResult, HttpServletResponse response){
if(bindingResult.hasErrors()){
return "login/loginForm";
}
Member loginMember = loginService.login(form.getLoginId(), form.getPassword());
if(loginMember == null){
bindingResult.reject("loginFail", "아이디 또는 비밀번호가 맞지 않습니다.");
return "login/loginForm";
}
// 세션 관리자를 통해 세션을 생성하고, 회원 데이터 보관
sessionManager.createSession(loginMember, response);
return "redirect:/";
}
@PostMapping("/logout")
public String logoutV2(HttpServletRequest request){
sessionManager.expire(request);
return "redirect:/";
}
}
로그인 처리과정의 핵심음 다음 한줄이다.
sessionManager.createSession(loginMember, response);
위와 같이 sessionManager에게 member 와 HttpServletResponse를 인자로 넘겨주면 로그인 성공시 세션 저장소에 member를 등록함과 동시에 session 쿠키또한 response에 담겨 클라이언트에게 전달된다.
이렇게 전달된 session 쿠키는 클라이언트 브라우저의 저장소에 저장되 있다, 이후 다른 요청을 서버에 하면 함께 전달된다.
위와같이 RequestHeader에 함께 전달되는것을 확인할 수 있다.
● LoginController - logoutV2()
@PostMapping("/logout")
public String logoutV2(HttpServletRequest request){
sessionManager.expire(request);
return "redirect:/";
}
로그아웃 할때도 sessionManager를 통해서 expire를 하게 된다.
expire 내부적으로 세션 저장소에서 member를 제거한다. 따라서 로그아웃시 해당 세션의 정보를 제거하게 된다.
● HomeController - homeLoginV2()
@Slf4j
@Controller
@RequiredArgsConstructor
public class HomeController {
private final MemberRepository memberRepository;
private final SessionManager sessionManager;
@GetMapping("/")
public String homeLoginV2(HttpServletRequest request, Model model){
// 세션 관리자에 저장된 회원 정보 조회
Member member = (Member)sessionManager.getSession(request);
if(member == null){
return "home";
}
model.addAttribute("member", member);
return "loginHome";
}
}
세션 관리자에서 저장된 회원 정보를 조회한다. 만약 회원 정보가 없으면, 쿠키나 세션이 없는 것 이므로 로그인 되지 않은 것으로 처리한다.
로그인 되지 않은 회원은 기본화면만 보여주며, 로그인 된 회원은 자신의 ID가 보이는 전용 페이지인 loginHome으로 이동한다.
8. 로그인 처리하기 - 서블릿 HTTP 세션1
서블릿은 세션을 위해 HttpSession 이라는 기능을 제공하는데, 지금까지 나온 문제들을 해결해준다.
우리가 직접 구현한 세션의 개념이 이미 구현되어 있고, 더 잘 구현되어 있다.
HttpSession의 경우 쿠키 이름이 JSESSIONID 이고 값은 추정 불가능한 랜덤값 이다.
Cookie: JSESSIONID=5B78E23B513F50164D6FDD8C97B0AD05
HttpSession을 활용하는 코드를 작성해 보자.
● LoginController - loginV3()
@Slf4j
@Controller
@RequiredArgsConstructor
public class LoginController {
private final LoginService loginService;
private final SessionManager sessionManager;
@GetMapping("/login")
public String loginForm(@ModelAttribute LoginForm form){
return "login/loginForm";
}
@PostMapping("/login")
public String loginV3(@Validated @ModelAttribute LoginForm form, BindingResult bindingResult, HttpServletRequest request){
if(bindingResult.hasErrors()){
return "login/loginForm";
}
Member loginMember = loginService.login(form.getLoginId(), form.getPassword());
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 logoutV3(HttpServletRequest request){
HttpSession session = request.getSession(false);
if(session != null){
session.invalidate();
}
return "redirect:/";
}
}
세션을 생성하기 위해 request.getSession(true)를 사용하면 됩니다.
이후 로그인한 회원 정보를 보관하기 위해 다음과 같이 사용하였습니다.
session.setAttribute(SessionConst.LOGIN_MEMBER, loginMember);
세션의 create 옵션에 대해 알아보자.
request.getSession(true)
- 세션이 있으면 기존 세션을 반환한다.
- 세션이 없으면 새로운 세션을 생성해서 반환한다.
request.getSession(false)
- 세션이 있으면 기존 세션을 반환한다.
- 세션이 없으면 새로운 세션을 생성하지 않는다. null 을 반환한다.
● LoginController - logoutV3()
@PostMapping("/logout")
public String logoutV3(HttpServletRequest request){
HttpSession session = request.getSession(false);
if(session != null){
session.invalidate();
}
return "redirect:/";
}
invalidate()를 사용하여 세션을 제거한다.
● HomeController - homeLoginV3()
@Slf4j
@Controller
@RequiredArgsConstructor
public class HomeController {
private final MemberRepository memberRepository;
private final SessionManager sessionManager;
@GetMapping("/")
public String homeLoginV3(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";
}
}
request.getSession(false) : request.getSession() 를 사용하면 기본 값이 create: true 이므로, 로그인 하지 않을 사용자도 의미없는 세션이 만들어진다. 이 또한 자원낭비라 할수있다.
따라서 세션을 찾아서 사용하는 시점에는 create: false 옵션을 사용해서 세션을 생성하지 않아야 한다.
궁금했던점!
이전에 직접 세션을 만들어서 사용하였을때는 key:value 쌍으로 (sessionId , loginMember) 를 사용했는데
session.setAttribute(SessionConst.LOGIN_MEMBER, loginMember);
위 코드를 사용하게 되면 sessionId 대신 SessionConst.LOGIN_MEMBER가 사용됩니다.
만약 그렇다면 고유한 SessionId는 어디서 사용되는 것 일까요?
1
서버는 수많은 클라이언트가 동시에 접속합니다. 그리고 이 클라이언트마다 고유한 세션아이디를 사용해야 겠죠?
그래서 서버의 특정 폴더( /etc) 에 sessionId 로 파일을 생성하여 보관합니다.
그리고 클라이언트가 쿠키로 세션아이디를 보내오면 이 아이디로 생성된 파일이 있는지 조회합니다.
sessionId 는 이 때 사용합니다.
직접 Session을 구현했을 때는 sessionId를 직접 만들어야 했지만, Session이라는 객체를 사용하면, Session 객체 내부에서 sessionId를 새로 발급하는 일 등을 알아서 처리합니다.
2 session.setAttribute(SessionConst.LOGIN_MEMBER, loginMember);
그리고 이 세션파일을 열면 그 안에 파일이 어떤 형식으로 저장되어 있는가의 문제가 바로 위의 코드 입니다.
SessionConst.LOGIN_MEMBER 복잡하고 긴 이름때문에 무섭게 생겼지만 사실은 상수값일 뿐입니다.
그러니 이 값을 100 이나 101 같은 숫자로 변경해서 사용하는것도 가능합니다.
이러한 숫자를 매직넘버라고 합니다. 매직넘버의 의미는 마법주문을 외우는 것처럼 그 의미를 알 수 없는 숫자라는 뜻입니다.
매직넘버로 값을 저장하게 되면 나중에 값을 찾아야 할 때 문제가 생깁니다, 어떤 숫자에 어떤 값을 넣어놨는지 의미를 파악할 수 없게 됩니다.("도대체 100이 의미하는게 뭐지??") 그래서 불편함을 위해 매직넘버를 사용하지 말고 상수를 이용하도록 권장하는 것입니다.
그래야 이 숫자가 무슨의미인지 알 수 없으니까요 (SessionConst.LOGIN_MEMBER 라는 값을 보면 누구나 '아! 로그인한 멤버를 저장했구나' 라고 이해하겠죠?)
9. 로그인 처리하기 - 서블릿 HTTP 세션2
@SessionAttribute
스프링은 세션을 더 편리하게 사용할 수 있도록 @SessionAttribute 을 지원한다.
이미 로그인 된 사용자를 찾을 때는 다음과 같이 사용하면 된다. 참고로 이 기능은 세션을 생성하지 않는다.
@SessionAttribute(name = "loginMember", required = false) Member loginMember
@GetMapping("/")
public String homeLoginV3Spring(
@SessionAttribute(name = SessionConst.LOGIN_MEMBER, required = false) Member loginMember, Model model){
if(loginMember == null){
return "home";
}
model.addAttribute("member", loginMember);
return "loginHome";
}
기존의 번거로운 검증과정을 스프링이 전부 관리해준다.
10. 세션 정보와 타임아웃 설정
우선 세션이 제공하는 여러 정보들을 확인해보자!
@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 = {}, value = {}", name, session.getAttribute(name)));
log.info("seesionId={}", session.getId());
log.info("getMaxInactiveInterval={}", session.getMaxInactiveInterval());
log.info("getCreationTime={}", new Date(session.getCreationTime()));
log.info("getLastAccessedTime={}", new Date(session.getLastAccessedTime()));
log.info("isNew={}", session.isNew());
return "세션 출력";
}
}
결과는 다음과 같다.
- sessionId : 세션Id, JSESSIONID 의 값이다. 예) 34B14F008AA3527C9F8ED620EFD7A4E1
- maxInactiveInterval : 세션의 유효 시간, 예) 1800초, (30분)
- creationTime : 세션 생성일시
- lastAccessedTime :세션과 연결된 사용자가 최근에 서버에 접근한 시간, 클라이언트에서 서버로 sessionId ( JSESSIONID )를 요청한 경우에 갱신된다.
- isNew : 새로 생성된 세션인지, 아니면 이미 과거에 만들어졌고, 클라이언트에서 서버로 sessionId ( JSESSIONID )를 요청해서 조회된 세션인지 여부
● 세션 타임아웃 설정
대부분의 사용자는 로그아웃을 누르지 않고 그냥 웹브라우저를 종료한다.
문제는 HTTP는 비연결성 이라서 서버 입장에서는 해당 사용자가 웹 브라우저를 종료한것 인지 아닌지를 인식할수가 없다.
이렇게 남아있는 세션을 무한정 보관하면 문제들이 발생하게 된다.
- 세션과 관련된 쿠키( JSESSIONID )를 탈취 당했을 경우 오랜 시간이 지나도 해당 쿠키로 악의적인 요청을 할 수 있다.
- 세션은 기본적으로 메모리에 생성된다. 메모리의 크기가 무한하지 않기 때문에 꼭 필요한 경우만 생성해서 사용해야 한다.
10만명의 사용자가 로그인하면 10만게의 세션이 생성되는 것이다. 이는 비용낭비이다.
● 세션의 종료 시점
그럼 어떤 기준으로 세션의 종료 시점을 정해야 할까?
세션을 생성시점이 아니라, 서버에 최근에 요청한 시간을 기준으로 30분 정도를 유지해주는 것 이다.
이렇게 하면 사용자가 서비스를 사용하고 있으면 지속적으로 서버로 요청이 전달되기 때문에, 세션의 생존 시간이 30분으로 계속 늘어난다.
따라서 30분 마다 로그인해야 하는 번거로움이 사라진다. HttpSession 은 이 방식을 사용한다.
● 세션 타임아웃 설정
- application.properties로 글로벌 설정
server.servlet.session.timeout=60
위 코드는 세션의 timeout을 60초 동안 유지시킨다. 일반적으로 1800(30분)을 사용한다.
● 세션 타임아웃 발생
위에서 session.getLastAccessedTime() : 최근 세션 접근 시간을 확인하였다.
최근 세션 접근시간 이후로 timeout 시간이 지나면, WAS가 내부에서 해당 세션을 제거해준다.
'BackEnd > Spring MVC' 카테고리의 다른 글
[Spring] 로그인 처리2 - 필터, 인터셉터 - 2 (0) | 2022.03.14 |
---|---|
[Spring] 로그인 처리2 - 필터, 인터셉터 - 1 (0) | 2022.03.13 |
[Spring] 로그인 처리1 - 쿠키, 세션 - 2 (0) | 2022.03.09 |
[Spring] 로그인 처리1 - 쿠키, 세션 - 1 (0) | 2022.03.09 |
[Spring] 검증2 - Bean Validation - 2 (0) | 2022.03.07 |
댓글