로그인 기능에 쿠키-세션을 적용시켜보자.
로그인을 하면 로그인 상태가 유지되어야하는데 이때 쿠키-세션을 사용할 것이다.
먼저 쿠키를 사용해서 개발을 하고 쿠키의 단점에 대해 알아볼 것이다.
그 다음 쿠키의 단점을 보완하기 위해 세션을 적용시킬 것.
사용자가 로그인을 하고 성공을 하면 서버측에서 쿠키를 생성해서 사용자의 웹브라우저로 보내주는데 이떼 서버측에서 HTTP 응답 메시지의 헤더의 set-cookie를 사용해서 쿠키를 보내줄 것이다.
그러면 웹브라우저는 쿠키를 캐시에 저장하고있다가 모든 요청에 있어서 쿠키가 자동으로 포함될 것이다.
쿠키는 2가지 종류가 있다.
1. 영속 쿠키 : 만료 날짜를 입력하면 해당 날짜까지 쿠키를 유지
// 영속 쿠키 생성 로직
Cookie userIdCookie = new Cookie("userId", String.valueOf(loginUser.getId()));
userIdCookie.setMaxAge(3000); //쿠키 만료 날짜 입력 ,단위는 "초"
response.addCookie(userIdCookie);
// 세션 쿠키 생성 로직
Cookie userIdCookie = new Cookie("userId", String.valueOf(loginUser.getId()));
response.addCookie(userIdCookie);
이 프로젝트에서는 유연함을 위해 세션 쿠키를 사용하겠다.
회원가입 및 로그인 후 Response Headers 내용
Connection: keep-alive
Content-Language: ko
Content-Length: 0
Date:Mon, 04 Sep 2023 09:25:22 GMT
Keep-Alive:timeout=60
Location:http://localhost:8080/library
Set-Cookie : userId=1
그 다움 요청을 보낼때마다 Request Header에 쿠키를 자동으로 포함한다.
@PostMapping("/login")
public String login(@Validated @ModelAttribute Login login, BindingResult bindingResult, HttpServletResponse response){
if (bindingResult.hasErrors()){
return "login/loginForm";
}
Optional<User> addUserCheck = userService.findByLoginId(login.getLoginId());
if (addUserCheck.isEmpty()) {
bindingResult.reject("addFail", "존재하지 않는 회원입니다. 회원가입을 해주세요.");
return "login/loginForm";
}
User loginUser = loginService.login(login.getLoginId(), login.getPassword());
if (loginUser == null) {
// 로그인 인증 과정에서 오류가 발생했을 때,
bindingResult.reject("loginFail", "아이디 또는 비밀번호가 맞지 않습니다. 다시 입력해주세요.");
return "login/loginForm";
}
// 세션 쿠키 생성 로직 및 로그인 성공 처리
Cookie userIdCookie = new Cookie("userId", String.valueOf(loginUser.getId()));
response.addCookie(userIdCookie);
return "redirect:/";
}
쿠키를 이용해서 사용자마다 자신이 회원가입할 때 등록한 name속성을 화면에 띄워주도록 해보자.
먼저 그러면 쿠키값을 가져와야하는데 서블릿 request를 이용해서 가져올 수 있지만 Spring에서는 @CookieValue라는 어노테이션을 제공해서 쿠키값을 쉽게 가져올 수 있도록 해준다.
required = false를 해준 이유는 로그인을 하지 않은 사람들도 접근해야하기 때문이다.
@GetMapping("/")
public String home(@CookieValue(name="userId", required = false) Long userId, Model model){
if (userId == null){
// 1. 로그인 사용자가 아닌 사용자들은 로그인 화면 보여주기
return "login/home";
}
// 로그인 한 사용자들
User loginUser = userService.findById(userId);
if (loginUser == null) {
// 2. 로그인을 성공하더라도 쿠키가 너무 오래되었거나 여러 이유로 사용자 조회가 안되는 경우를 대비
return "login/home";
}
// 로그인 성공 시 home화면에 사용자 이름 보여주기.
model.addAttribute("loginUser", loginUser);
return "login/home";
}
home.html 수정 <body>태그 코드
<body>
<div th:if="${loginUser != null}">
<h3 th:text="|${loginUser.name}님. 반갑습니다.|"></h3>
</div>
<div>
<button type="button" th:onclick="|location.href='@{/login}'|" th:if="${loginUser == null}">로그인</button>
<button type="button" th:onclick="|location.href='@{/add/user}'|" th:if="${loginUser == null}">회원가입</button>
<button type="button" th:onclick="|location.href='@{/library}'|" th:if="${loginUser != null}">서비스 이용</button>
<form th:action="@{/logout}" method="post" th:if="${loginUser != null}">
<button type="submit" >로그아웃</button>
</form>
</div>
</body>
로그인 되었을 때의 폼을 따로 만들어도 되지만 타임리프의 조건문을 활용해서 home.html을 재활용했다.
loginUser가 null이 아닌 경우 서비스 이용과 로그아웃 버튼을 보여주게 했다.
로그아웃 같은 경우는 Post를 사용할 것이어서 form태그 안에 넣어줬다.
로그아웃 기능은 Post로 하는 이유는 아래에서 자세히 알아보겠다.
처음 Home 화면
로그인 시 Home 화면
서비스 이용 시 /library 경로로 이동 후 도서 관리 서비스 이용 가능
로그아웃
로그아웃 기능은 새로운 쿠키를 생성하고 그 쿠기의 만료 기간을 0으로 설정해주고 response에 만료 기간이 0인 쿠키를 추가해줌으로써 로그아웃을 하도록 구현했다.
왜냐하면 클라이언트가 로그인 후 다른 요청을 보낼 때 함께 보내는 쿠키는 클라이언트 브라우저에 저장되어있는 쿠키의 복사본이다.
그렇기때문에 원본 쿠키를 만료기간이 0은 새로운 쿠키로 덮어쓰게 해야한다.
@PostMapping("/logout")
public String logout(HttpServletResponse response){
// 쿠키를 종료시킬꺼면 쿠키의 만료시간을 0으로
Cookie cookie = new Cookie("userId", null);
cookie.setMaxAge(0);
response.addCookie(cookie);
return "redirect:/";
}
기존의 로그인, 로그아웃 기능을 쿠키를 적용시켜보았다.
하지만 쿠키는 보안적으로 위험한데 그 이유에 대해 알아보자.
등의 이유로 쿠키는 안전하지 않다.
그래서 쿠키의 이러한 단점들을 보완하고자 세션이 등장했다.
이처럼 중요한 정보는 클라이언트에 노출되지않고 서버가 관리하고 임의의 랜덤값(세션ID)를 통해 사용자를 식별하여 연결을 하는 것을 세션이라고 한다.
서블릿은 HttpSession이라는 기능을 제공해주는데 이를 가지고 세션을 적용시켜보자.
//세션이 있으면 있는 세션을 반환하고, 없으면 신규 세션을 생성함.
HttpSession session = request.getSession();
//세션에 로그인 회원 정보 보관
session.setAttribute("loginUser", loginUser );
@PostMapping("/logout")
public String logout(HttpServletRequest request){
HttpSession session = request.getSession(false);
if (session != null) {
session.invalidate();
}
return "redirect:/";
}
}
@GetMapping("/")
public String homeV2(HttpServletRequest request, Model model){
HttpSession session = request.getSession(false);
if (session == null){
return "login/home";
}
User loginUser = (User)session.getAttribute("loginUser");
if (loginUser == null) {
// 세션에 user 데이터가 없는 경우
return "login/home";
}
// 성공
model.addAttribute("loginUser", loginUser);
return "login/home";
}
Spring이 제공하는 어노테이션을 사용해서 코드를 다듬어보자.
@GetMapping("/")
public String homeV2(@SessionAttribute(name="loginUser", required = false) User loginUser, Model model){
if (loginUser == null) {
// 세션에 user 데이터가 없는 경우
return "login/home";
}
// 성공
model.addAttribute("loginUser", loginUser);
return "login/home";
}
참고)쿠키를 통해서만 세션을 유지하기 위해 다음과 같이 설정.
server.servlet.session.tracking-modes=cookie
server.servlet.session.timeout=1800
session.setMaxInactiveInterval(1800)
세션을 사용해서 쿠키의 취약점도 보완했다.
그러면 이제 보안에서 완벽할까?
아니다.
로그인을 하지 않더라도 도서 관리 서비스를 사용할 수 있다.
그냥 url경로에 http://localhost:8080/library를 치면 로그인을 안하더라도 사용할 수 있다.
그래서 로그인을 하지 않는 사용자에 대해서 필터링을 거는 것에 대해서 구현할 것.