모르지 않다는 것은 아는것과 다르다.

Spring

쿠키와 세션을 사용한 로그인 처리

채마스 2022. 2. 26. 09:56

쿠키

  • 쿠키에는 영속 쿠키와 세션 쿠키가 있다.
    • 영속 쿠키 : 만료 날짜를 입력하면 해당 날짜까지 유지
    • 세션 쿠키 : 만료 날짜를 생략하면 브라우져 종료시 까지만 유지
  • 쿠키생성 로직
    Cookie idCookie = new Cookie("memberId", String.valueOf(loginMember.getId()));
    response.addCookie(idCookie);
  • 로그인에 성공하면 쿠키를 생성하고 HttpServletResponse 에 담는다.
  • 쿠키 이름은 memberId 이고, 값은 회원의 id를담아둔다.
  • 웹브라우저는종료전까지회원의 id를서버에계속보내줄것이다.
    • request header 를 보면 Cookie: memberId=1 이 있는 것을 확인할 수 있다.
    • Application -> Storage -> Cookies 에 가봐도 볼 수 있다.
  • 쿠키를 이용한 로그인 처리
  @GetMapping("/")
    public String homeLogin(@CookieValue(name = "memberId", required = false) Long memberId,
        Model model) {
      if (memberId == null) {
          return "home";
      }
    //로그인
    Member loginMember = memberRepository.findById(memberId); 
    if (loginMember == null) {
        return "home";
      }
        model.addAttribute("member", loginMember);
        return "loginHome";
    }
- 위와 같이 @CookieValue 로 쿠키를 가져올 수 있다.
- 로그인 하지 않은 사용자도 화면을 조회할 수 있어야 되기 때문에 required = false 로 설정해 준다.
  • 쿠키를 이용한 로그아웃 처리
  @PostMapping("/logout")
  public String logout(HttpServletResponse response) {
      expireCookie(response, "memberId");
      return "redirect:/";
  }
  private void expireCookie(HttpServletResponse response, String cookieName) {
      Cookie cookie = new Cookie(cookieName, null);
      cookie.setMaxAge(0);
      response.addCookie(cookie);
}
- 서버에서 해당 쿠키의 종료 날짜를 0으로 지정한다.
- 로그아웃도 응답 쿠키를 생성하는데 Response Headers를 확인해 보면 `Set-Cookie: Max-Age=0;` 를 확인할 수 있다.
- 또한 Application -> Storage -> Cookies 에 가봐도 쿠키가 삭제된 것을 확인할 수 있다.

쿠키와 보안 문제

  • 위와 같은 구현방식은 엄청 심각한 보안 문제가 있다.
  • 대표적인 문제들은 아래와 같다.
    • 쿠키 값은 임의로 변경할 수 있다.
    • 쿠키에 보관된 정보는 훔쳐갈 수 있다.
    • 해커가 쿠키를 한번 훔쳐가면 평생 사용할 수 있다.
  • 해결 방안
    • 쿠키에 중요한 값을 노출하지 않고, 사용자 별로 예측 불가능한 임의의 토큰(랜덤 값)을 노출한다.
    • 서버에서 토큰과 사용자 id를 매핑해서 인식하고 서버에서 토큰을 관리한다.
    • 큰은 해커가 임의의 값을 넣어도 찾을 수 없도록 예상 불가능 해야 한다.
    • 해커가 토큰을 털어가도 시간이 지나면 사용할 수 없도록 서버에서 해당 토큰의 만료시간을 짧게 유지한다.

세션

  • 서버에 중요한 정보를 보관하고 연결을 유지하는 방법을 세션이라고 한다.

  • 세션 ID를 생성하는데, 추정 불가능해야 한다 -> UUID로 생성하기 때문이다.
  • 생성된 세션 ID와 세션에 보관할 값( memberA )을 서버의 세션 저장소에 보관한다.
  • 서버는 클라이언트에 mySessionId 라는 이름으로 세션ID 만 쿠키에 담아서 전달한다.
  • 클라이언트는 쿠키 저장소에 mySessionId 쿠키를 보관한다.
  • 클라이언트는 요청시 항상 mySessionId 쿠키를 전달한다.
  • 서버에서는 클라이언트가 전달한 mySessionId 쿠키 정보로 세션 저장소를 조회해서 로그인시 보관한 세션 정보를 사용한다.
  • 이런식으로 구현하면 쿠키만 사용했을 때 발생할 수 있는 보안 문제를 해결할 수 있다.
  • 하지만 세션에는 최소한의 데이터만 보관해야한다.
    • 객체를 넣지말고, 아이디만 넣는식으로 메모리 관리는 필수이다.
  • HttpSession 을 통한 로그인 처리
    • 서블릿은 세션을 위해 HttpSession 이라는 기능을 제공해 준다.
    • 로그인 기능 구현
  @PostMapping("/login")
  public String loginV3(@Valid @ModelAttribute LoginForm form, BindingResult bindingResult, HttpServletRequest request) {
    if (bindingResult.hasErrors()) {
        return "login/loginForm";
    }
    Member loginMember = loginService.login(form.getLoginId(), form.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:/";
  }
- request.getSession(true)
  - 세션이 있으면 기존 세션을 반환한다.
  - 세션이 없으면 새로운 세션을 생성해서 반환한다.
- request.getSession(false)
  - 세션이 있으면 기존 세션을 반환한다.
  - 세션이 없으면 새로운 세션을 생성하지 않는다. -> null 을 반환한다.
- session.setAttribute(SessionConst.LOGIN_MEMBER, loginMember); 을 통해서 세션 정보를 저장한다.
  • 로그아웃 기능 구현
  @PostMapping("/logout")
  public String logoutV3(HttpServletRequest request) {
  //세션을 삭제한다.
  HttpSession session = request.getSession(false); if (session != null) {
        session.invalidate();
  }
    return "redirect:/";
  }
  • session.invalidate() : 세션을 제거한다.
  • 홈 화면에서 로그인 기능 구현
  @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) -> true 로 설정할 경우, 로그인 하지 않을 사용자도 의미없는 세션이 만들어진다.
  • session.getAttribute(SessionConst.LOGIN_MEMBER) -> 로그인 시점에 세션에 보관한 회원 객체를 찾는다.
  • 위와 같은 기능을 스프링에서 제공하는 @SessionAttribute를 사용하면 더 간단하게 구현이 가능하다.
  @GetMapping("/")
  public String homeLoginV3Spring( @SessionAttribute(name = SessionConst.LOGIN_MEMBER,       required = false) Member loginMember,Model model) {
    //세션에 회원 데이터가 없으면 home 
    if (loginMember == null) {
      return "home";
    }
    //세션이 유지되면 로그인으로 이동 
    model.addAttribute("member", loginMember); 
    return "loginHome";
  }
  • 이 기능은 세션을 생성하지 않는다.

세션 제공하는 정보

  @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() 가 호출 되는 경우에 삭제된다.
  • 하지만 대부분의 사용자는 로그아웃을 하지 않고 종료한다. -> 그렇기 때문에 세션에 타임아웃을 꼭 설정해 줘야한다.
  • 세션의 종료시점은 최근에 요청한 시간을 기준으로 30분 정도를 유지해주는 것이 좋다.
  • 설정 방법
    • 글로벌 -> application.properties 파일에 server.servlet.session.timeout=1800 를 적어준다.
    • 특정 세션 단위로 시간 설정 -> session.setMaxInactiveInterval(1800);

REFERENCES

  • 김영한님의 스프링 MVC 2편

'Spring' 카테고리의 다른 글

필터와 인터셉터  (0) 2022.02.27
파일 업로드  (0) 2022.02.27
컴포넌트스캔  (0) 2022.02.26
커스텀 Validator 구현  (0) 2022.02.26
의존관계 주입  (0) 2022.02.26