[사이드 프로젝트] 나만의 도서 관리 서비스6-2

zwon·2023년 9월 4일
0

개발일지

목록 보기
10/23

로그인 기능에 쿠키-세션을 적용시켜보자.
로그인을 하면 로그인 상태가 유지되어야하는데 이때 쿠키-세션을 사용할 것이다.
먼저 쿠키를 사용해서 개발을 하고 쿠키의 단점에 대해 알아볼 것이다.
그 다음 쿠키의 단점을 보완하기 위해 세션을 적용시킬 것.


사용자가 로그인을 하고 성공을 하면 서버측에서 쿠키를 생성해서 사용자의 웹브라우저로 보내주는데 이떼 서버측에서 HTTP 응답 메시지의 헤더의 set-cookie를 사용해서 쿠키를 보내줄 것이다.

그러면 웹브라우저는 쿠키를 캐시에 저장하고있다가 모든 요청에 있어서 쿠키가 자동으로 포함될 것이다.

쿠키 생성

쿠키는 2가지 종류가 있다.
1. 영속 쿠키 : 만료 날짜를 입력하면 해당 날짜까지 쿠키를 유지

// 영속 쿠키 생성 로직
    Cookie userIdCookie = new Cookie("userId", String.valueOf(loginUser.getId()));
    userIdCookie.setMaxAge(3000); //쿠키 만료 날짜 입력 ,단위는 "초"
    response.addCookie(userIdCookie);
  1. 세션 쿠키 : 만료 날짜를 설정하지 않으면 브라우저 종료시 까지만 쿠키 유지
// 세션 쿠키 생성 로직
    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:/";
  }
  • 로그아웃 기능은 Post로 하는 이유
    • 우선 Spring security에서 로그아웃 기능은 Post로 되어 있으며,
    • Get 방식을 사용할 경우 의도치 않게 사용자를 로그아웃 시킬 수 있다.
      • 이 부분에 대해서는 추가적인 공부가 필요해 보인다.

기존의 로그인, 로그아웃 기능을 쿠키를 적용시켜보았다.
하지만 쿠키는 보안적으로 위험한데 그 이유에 대해 알아보자.


쿠키가 안전하지 않는 이유

  1. 쿠키는 사용자가 관리한다.
  2. 쿠키 값은 변경이 가능하다.
    • F12->Application->storage->Cookies에서 쉽게 변경이 가능하다.
  3. 쿠키는 쉽게 탈취가 가능하다.
    • 웹브라우저에서 보관된다. -> 해킹을 당하면?
    • 개인을 식별할 수 있는 정보나 민감한 정보가 쿠키에 담겨있는 경우 정보 유출 등의 위험이 있다.

등의 이유로 쿠키는 안전하지 않다.

그래서 쿠키의 이러한 단점들을 보완하고자 세션이 등장했다.


세션 Session

  1. 쿠키 기반이지만 서버에서 저장 및 관리한다.
  2. 쿠키에 개인과는 전혀 상관없는 임의의 랜덤값(세션ID)으로 설정하고 서버에서는 이 세션ID와 사용자를 매핑한다.
    • 의미없는 임의의 랜덤값이어서 탈취를 당해도 정보 유추가 불가능하다.

이처럼 중요한 정보는 클라이언트에 노출되지않고 서버가 관리하고 임의의 랜덤값(세션ID)를 통해 사용자를 식별하여 연결을 하는 것을 세션이라고 한다.

세션 동작 과정

  1. 로그인을 한다.
  2. 로그인을 성공시 세션ID 생성
    • Java는 UUID를 통해 세션ID를 생성할 것
  3. 생성한 세션ID를 세션 저장소에 세션ID와 매핑할 값을 보관
  4. 세션ID를 HTTP 응답 메시지 헤더에 저장
  5. 사용자는 쿠키 저장소에 세션ID를 저장
  6. 사용자는 요청할 때마다 세션ID를 자동으로 포함하고 세션ID를 통해 사용자를 식별한다.

서블릿은 HttpSession이라는 기능을 제공해주는데 이를 가지고 세션을 적용시켜보자.

세션을 이용한 로그인

//세션이 있으면 있는 세션을 반환하고, 없으면 신규 세션을 생성함.
    HttpSession session = request.getSession();
    //세션에 로그인 회원 정보 보관
    session.setAttribute("loginUser", loginUser );
  • request.getSession();은 세션을 생성한다.
    • request.getSession(true)
      • 세션이 있으면 있는 세션을 반환하고, 없으면 신규 세션을 생성함.
      • 기본값이 true
    • request.getSession(false)
      • 세션이 있으면 있는 세션을 반환하고, 없으면 null을 반환함.

세션을 이용한 로그아웃

@PostMapping("/logout")
  public String logout(HttpServletRequest request){
    HttpSession session = request.getSession(false);
    if (session != null) {
      session.invalidate();
    }
    return "redirect:/";
  }
    }
  • false로 해야 세션이 없더라도 세션을 새로 생성하지 않는다.
  • session.invalidate()를 통해서 해당 사용자의 모든 세션 정보가 삭제된다.

로그인 시 home

@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";
  }
  • 여기서도 마찬가지로 false로 해야 세션이 없더라도 세션을 새로 생성하지 않는다.
  • getAttribute()를 통해 사용자 값을 가져온다.

Response Headers

Request Headers


Spring이 제공하는 어노테이션을 사용해서 코드를 다듬어보자.

세션을 이용한 로그인

세션을 이용한 로그아웃

로그인 시 home

  • @SessionAttribute를 사용해서 세션에서 name=loginUser를 찾아 꺼내서 user에 값을 넣어준다.
 @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
  • 이렇게 설정해줌으로써 첫 로그인시 url에 JSESSIONID=.........가 붙지 않는다.
  • url에 JSESSIONID=.........가 붙는 이유는 웹 브라우저가 쿠키를 지원하지 않을 때 URL을 통해 세션을 유지하는 방법인데 .... 약간 번거롭다.

세션 타임아웃 설정

  • 위에서 로그아웃을 클릭해야만 세션을 삭제할 수 있었는데 로그아웃 버튼을 눌러서 로그아웃하기보다는 사용자가 그냥 브라우저 창을 닫는 경우가 더 많을 것이다.
  • 그래서 세션에 타임아웃 설정을 해서 무한정 연결이 되는 것을 방지할 수 있다.

그렇다면 세션은 언제 종료시켜야할까?

  • HttpSession은 사용자가 최근에 서버에 요청한 시간을 기준으로 시간을 늘려주는 방식으로 세션을 관리한다.
  • 예를들어 30분이 지나면 로그아웃이 되도록 타임아웃을 설정 했을 때, 세션 생성 시점부터 30분이면 열심히 서비스를 이용하다가 30분마다 로그인을 해야하는 번거로움이 있다.
  • 그렇기 때문에 서버에 요청한 시간 중 최신 시간을 기준으로 +30분씩 늘려주면 된다.

세션 타임아웃 설정 - 글로벌

  • appication properties에 다음과 같이 설정할 수 있다.
  • 기본이 초 단위이며, 1800초(30분)가 기본값이다.
server.servlet.session.timeout=1800

세션 타임아웃 설정 - 특정 세션

  • 세션을 생성한 후 setMaxInactiveInterval()를 사용해서 설정할 수 있다.
session.setMaxInactiveInterval(1800)

세션을 사용해서 쿠키의 취약점도 보완했다.
그러면 이제 보안에서 완벽할까?

아니다.

로그인을 하지 않더라도 도서 관리 서비스를 사용할 수 있다.
그냥 url경로에 http://localhost:8080/library를 치면 로그인을 안하더라도 사용할 수 있다.

그래서 로그인을 하지 않는 사용자에 대해서 필터링을 거는 것에 대해서 구현할 것.

profile
Backend 관련 지식을 정리하는 Back과사전

0개의 댓글