[Spring MVC] 스프링 MVC - 로그인 (쿠키, 세션)

홍정완·2022년 12월 18일
0

Spring

목록 보기
31/32
post-thumbnail

개요


이번 포스팅에서는 쿠키와 세션을 통해 로그인을 처리하는 과정을 알아보자.



쿠키를 사용한 로그인 처리


로그인의 상태를 어떻게 유지할 수 있을까 ❓

HTTP 강의에서 일부 설명했지만, 쿼리 파라미터를 계속 유지하면서 보내는 것은 매우 어렵고 번거로운
작업이다. 쿠키를 사용해 보자.

서버에서 로그인에 성공하면 HTTP 응답에 쿠키를 담아서 브라우저에 전달하자. 그러면 브라우저는 앞으로 해당 쿠키를 지속해서 보내준다.


로그인 시 쿠키 생성클라이언트에서 쿠키 전달



쿠키의 종류

사용자는 상황에 따라 입맛에 맞게끔 쿠키의 생명주기를 설정해 사용할 수 있다.

  • 영속 쿠키 : 만료 날짜를 입력하면 해당 날짜까지 유지
  • 세션 쿠키 : 만료 날짜를 생략하면 브라우저 종료 시까지만 유지

브라우저 종료 시 로그아웃이 되길 기대하므로, 우리에게 필요한 것은 세션 쿠키이다.



스프링 부트에서 쿠키 핸들링

이제 스프링 부트를 이용해 서버에서 쿠키를 생성 및 조회를 해보자.
java.servlet.http에는 Cookie라는 클래스를 제공해 주는데 이 클래스를 이용해 클라이언트에 응답할 쿠키 정보를 쉽게 핸들링할 수 있다.


LoginController - login()

@PostMapping("login")
public String login(@Valid @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";
    }

    // 쿠키에 시간 정보를 주지 않으면 세션 쿠키가 된다. (브라우저 종료 시 모두 종료)
    Cookie idCookie = new Cookie("memberId", String.valueOf(loginMember.getId()));
    response.addCookie(idCookie);

    return "redirect:/";
}

new Cookie("memberId", String.valueOf(loginMember.getId()));

  • Cookie라는 클래스 생성자로 key / value를 인수로 넘겨주어 생성한다.

response.addCookie(idCookie);

  • 생성된 쿠키(idCookie)를 서버 응답 객체(HttpServletResponse)에 addCookie를 이용해 담아준다. 그럼 실제로 웹 브라우저에서는 Set-Cookie 프로퍼티에 쿠키 정보가 담겨 반환된다.



홈 - 로그인 처리

@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(name = "memberId", required = false) Long memberId

  • 쿠키를 편하게 조회할 수 있도록 도와주는 애노테이션이다. 전송된 쿠키 정보중 keymemberId인 쿠키 값을 찾아 memberId 변수에 할당해 준다. requiredfalse이기에 쿠키 정보가 없는 비회원도 접근 가능하다.



서버에서 쿠키 없애기 (로그아웃)

로그인을 했으면 로그아웃도 있어야 한다. 로그아웃 기능은 쿠키를 삭제하는 게 아니라 종료 날짜를 0으로 줘서 바로 만료시킴으로써 삭제할 수 있다.


@PostMapping("/logout")
public String logout(HttpServletResponse response) {
    expiredCookie(response, "memberId");
    
    return "redirect:/";
}

private void expiredCookie(HttpServletResponse response, String cookieName) {
    Cookie cookie = new Cookie(cookieName, null);
    cookie.setMaxAge(0);
    
    response.addCookie(cookie);
}

  • 응답 쿠키의 정보를 보면 Max-Age=0으로 되어있어 해당 쿠키는 즉시 종료된다.



쿠키와 보안 문제

쿠키는 아래와 같은 보안 문제점을 가지고 있다.

  • 쿠키 값을 임의대로 변경할 수 있다.
  • 쿠키에 보관된 정보(memberId)를 타인이 훔쳐 갈 수 있다.
  • 한 번 도용된 쿠키 정보는 계속 악용될 수 있다.

문제점을 정리해 보면 쿠키는 위변조 및 도용이 쉽다는 문제가 있다.

이 문제를 해결하려면 결국 중요한 정보를 모두 서버에 저장해야 한다. 그리고 클라이언트와 서버는 추정 불가능한 임의의 식별자 값으로 연결해야 한다.

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



세션을 통한 로그인 처리



세션으로 동작하는 로그인

위에서 했던 내용을 요약하면 중요 정보는 서버의 세션 저장소에 key / value로 저장한 뒤 브라우저에서는 key 값만 가지고 있도록 하는 것이다.


세션 기반 로그인 과정

이제 다음 로그인이나 페이지 접근 시 쿠키에선 저장하고 있는 sessionId를 같이 전달하면 서버의 세션 저장소에서는 해당 sessionIdkey로 가지고 있는 value 값을 조회해서 로그인 여부와 중요 정보를 확인한다.


  • 회원과 관련된 정보는 클라이언트에서 가지고 있지 않다.
  • 추정 불가능한 세션 아이디만 쿠키를 통해 주고받기에 보안에서 많이 안전해졌다.

여기서 더하여 세션 아이디가 저장된 쿠키의 만료시간을 짧게 유지한다면, 해커가 해당 키를 도용한다 하더라도 금세 갱신되며 사용하지 못하게 되어 보안적으로 좀 더 안전해질 수 있다.



세션 관리 기능

이러한 세션은 다음과 같이 크게 3가지 기능을 제공해야 한다.


  • 세션 생성
    • 세션 키는 중복이 안되며 추정 불가능한 랜덤 값이어야 한다.
    • 세션 키에 매칭될 값(value)이 있어야 한다.
    • 이렇게 생성된 세션 키를 응답 쿠키에 저장해 클라이언트에 전달해야 한다.
  • 세션 조회
    • 클라이언트가 요청한 세션 아이디 쿠키 값으로 세션 저장소에 저장된 값을 조회할 수 있어야 한다.
  • 세션 만료
    • 클라이언트가 요청한 세션 아이디 쿠키 값으로 세션 저장소에 보관한 세션 엔트리를 제거해야 한다.



서블릿 HttpSession을 이용한 로그인 처리


서블릿을 통해 HttpSession을 생성하면 다음과 같은 쿠키를 생성한다. 쿠키 이름이 JSESSIONID이고, 값은 추정 불가능한 랜덤 값이다.

Cookie: JSESSIONID=5B78E23B513F50164D6FDD8C97B0AD05


HttpSession을 사용하는 코드


세션 조회용 상수

public interface SessionConst {
	String LOGIN_MEMBER = "loginMember";
}

로그인 컨트롤러

@PostMapping("login")
public String loginV3(@Valid @ModelAttribute LoginForm form, BindingResult bindingResult, 
					  HttpServletResponse response, 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(HttpServletResponse response, HttpServletRequest request) {
    HttpSession session = request.getSession(false);
    
    if (session != null) session.invalidate();
    
    return "redirect:/";
}

request.getSession()

  • getSession 메서드는 세션을 생성 혹은 조회하는 메서드이다.
    public HttpSession getSession(boolean create); // default true

여기서 create 옵션의 의미는 다음과 같다.

  • request.getSession(true)일 경우

    • 세션이 있으면 기존 세션을 반환한다.
    • 세션이 없으면 새로운 세션을 생성해 반환한다.
  • request.getSession(false)일 경우

    • 세션이 있으면 기존 세션을 반환한다.
    • 세션이 없으면 새로운 세션을 생성하지 않고 null을 반환한다.

추가적으로 인수를 전달하지 않을 경우 기본 값으로 true이다.

session.invalidate();

  • 세션을 제거하는 메서드



@SessionAttribute 애노테이션 활용

스프링은 세션을 더 편리하게 사용할 수 있도록 @SessionAttribute라는 애노테이션을 제공한다.


@GetMapping("/")
public String homeLoginV3Spring(
        @SessionAttribute(name = SessionConst.LOGIN_MEMBER, required = false)Member loginMember,
        HttpServletRequest request, Model model) {

    if (loginMember == null) return "home";
    model.addAttribute("member", loginMember);
    
    return "loginHome";
}

@SessionAttribute(name = SessionConst.LOGIN_MEMBER, required = false)

  • 이전에 사용한 @CookieValue와 비슷하다. 클라이언트로부터 전달받은 내용의 세션 중에서 key가 일치하는 게 있는지 찾는다. requiredfalse이니 만약 못 찾으면 null이 할당될 것이다.



TrackingModes

로그인을 처음 시도하면 URL이 다음과 같이 jsessionid를 포함하고 있는 것을 확인할 수 있다.

http://localhost:8080/;jsessionid=F5511518B921DF6209l.......

이것은 웹 브라우저가 쿠키를 지원하지 않을 때 쿠키 대신 URL을 통해서 세션을 유지하는 방법이다. 이 방법을 사용하려면 URL에 이 값을 계속 포함해서 전달해야 한다. 타임리프 같은 템플릿은 엔진을 통해서 링크를 걸면 jsessionid를 URL에 자동으로 포함해 준다. 서버 입장에서 웹 브라우저가 쿠키를 지원하는지 하지 않는지 최초에는 판단하지 못하므로, 쿠키 값도 전달하고, URL에 jsessionid도 함께 전달한다.

URL 전달 방식을 끄고 항상 쿠키를 통해서만 세션을 유지하고 싶으면 다음 옵션을 넣어주면 된다. 이렇게 하면 URL에 jsessionid가 노출되지 않는다

server.servlet.session.tracking-modes=cookie



HttpSession에서 제공하는 정보

HttpSession에서는 많은 세션 정보를 제공하는데 다음과 같다.

public void printSessionInfo(HttpServletRequest request, String sessionId){
	HttpSession session = request.getSession(false);    

    log.info("sessionId={}", session.getId());
    log.info("getMaxInactiveInterval={}", session.getMaxInactiveInterval());
    log.info("creationTime={}", new Date(session.getCreationTime()));
    log.info("lastAccessedTime={}", new Date(session.getLastAccessedTime()));
    log.info("isNew={}", session.isNew());
}

  • sessionId : 세션 아이디(JSESSIONID)의 값(ex:754BE5D4DD969894D958AC278370D06E)

  • maxInactiveInterval : 세션의 유효 시간 (ex: 1800초)

  • creationTime : 세션 생성 일시

  • lastAccessedTime : 세션과 연결된 사용자가 최근에 서버에 접속한 시간.
    (클라이언트에서 서버로 sessionId(JSESSIONID)를 요청한 경우 갱신된다.)

  • isNew : 새로 생성된 세션인지, 아니면 이미 과거에 만들어졌고, 클라이언트에서 서버로 sessionId(JSESSIONID)를 요청해서 조회된 세션인지 여부



세션 타임아웃 설정하기

대부분의 사용자는 직접 명시적으로 로그아웃 버튼을 누르지 않는다.

그냥 웹 브라우저를 종료할 뿐인데 HTTP는 비 연결성(ConnectionLess)이기에 서버 측에선 클라이언트가 웹 브라우저를 종료했는지를 알 수 없다. 그렇기에 세션을 언제 삭제해야 할지 판단하기 어렵다.

그렇다고 세션을 무한정 유지되도록 한다면, 여러 가지 문제가 발생할 수 있다.


  • JSESSIONID를 탈취당한 경우 시간이 흘러도 해당 쿠키로 악용될 수 있다.

  • 세션은 기본적으로 메모리에 생성되는데 메모리의 크기가 무한하지 않기에 사용하지 않는 세션이 관리되지 않으면 성능 저하가 필연적이고 OutOfMemoryException이 발생할 수 있다.


이러한 이유로 세션에는 타임아웃이 되어야 하는데, 종료 시점은 어떻게 설정하는 게 좋을까 ❓

너무 빠르면 로그인 유지가 무관하게 계속 로그인을 해야 한다. 그렇다고 너무 길게 잡으면 위에 말한 문제 문제가 발생한다. 기본적으로는 세션 생성 시점으로부터 30분 정도를 잡고는 한다.

하지만, 여기서 문제가 한 가지 더 있다. 종료 시점을 30분으로 둔다고 하면 사용자가 30분간 활동하다가 다시 로그인을 해야 하는 것일까 ❓ 이보다는 사용자가 가장 최근 요청한 시간을 기준으로 30분 정도를 유지하는 것이다.


HttpSession은 기본적으로 이 방식을 사용하는데 기획에 따라 이 설정을 변경할 수도 있다.

스프링 부트에서는 application.properties에 글로벌 설정을 해 줄 수 있다.

session.setMaxInactiveInterval(1800); // 1800초

이렇게 1800초(30분)으로 설정을 해두면 LastAccessTime 이후 timeout 시간이 지나면 WAS 내부에서 해당 세션을 삭제한다.



✅ 정리


서블릿의 HttpSession이 제공하는 타임아웃 기능 덕분에 세션을 안전하고 편리하게 사용할 수 있다.

실무에서 주의할 점은 세션에는 최소한의 데이터만 보관해야 한다는 점이다. 보관한 데이터 용량 * 사용자 수로 세션의 메모리 사용량이 급격하게 늘어나서 장애로 이어질 수 있다. 추가로 세션의 시간을 너무 길게 가져가면 메모리 사용이 계속 누적될 수 있으므로 적당한 시간을 선택하는 것이 필요하다. 기본이 30분이라는 것을 기준으로 고민하면 된다.

profile
습관이 전부다.

0개의 댓글