[Spring] 쿠키/세션으로 로그인 처리하기 (세션 저장소 만들기, HttpSession)

728x90

 

쿠키

로그인

사용자가 로그인에 성공하면 응답에 쿠키를 생성하여 리다이렉트 한다.

@PostMapping("/login")
public String login(@Valid @ModelAttribute LoginForm form,
                    BindingResult bindingResult, HttpServletResponse response) {
  ...
  Cookie idCookie = new Cookie("memberId", String.valueOf(loginMember.getId()));
  response.addCookie(idCookie);

  return "redirect:/";
}

 

세션 쿠키로 설정하기 위해 쿠키에 시간 정보를 주지 않았다.

 

`영속 쿠키` : 만료 날짜까지 유지

`세션 쿠키` : 브라우저 종료 시까지만 유지

 

@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` 를 사용한다.

 

1. 로그인 쿠키 `memberId` 가 없는 사용자와 로그인 쿠키가 있어도 회원을 찾을 수 없는 사용자는 `home` 으로 보낸다.

2. 로그인 쿠키 `memberId` 가 있는 회원 사용자는 로그인 사용자 전용 홈 화면 `loginHome` 으로 보낸다.

이때, model 에 `member` 데이터를 전달하여 회원 관련 정보를 출력할 수 있도록 한다.

 

로그아웃

@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);
}

 

여기서 `Max-Age = 0` 으로 하여 쿠키를 새로 생성하는 이유는 다음과 같다.

 

요청에서의 사용자 쿠키는 사용자의 브라우저에 저장된 쿠키의 복사본이므로, 해당 쿠키를 사용하여 만료시킬 수 없다.

따라서 서버에서 동일한 이름으로 쿠키를 만들어 `Max-Age = 0` 으로 설정한 뒤,

브라우저에 저장된 쿠키를 새로 만든 쿠키로 덮어쓰도록 해야 한다.

 

문제점

- 쿠키값은 클라이언트에서 서버로 전달하는 것이기 때문에 위변조가 가능하다.

- 쿠키에 보관된 정보는 보안이 되지 않는다.

- 해커가 쿠키를 탈취하면 평생 사용할 수 있다.

 

따라서 쿠키만을 사용할 수는 없다.

 

세션

중요한 정보들은 모두 서버에서 관리하고, 쿠키 값으로 추정 불가능한 임의의 식별자 값을 주면 된다.

 

세션

서버에 중요한 정보를 보관하고 연결을 유지하는 방법

 

서버에 세션 저장소를 만들고,

세션 저장소에 토큰 키(세션 ID) 를 `UUID` 로 생성하여 키로 사용하고 값은 객체를 저장한다.

(UUID 는 중복이 되지 않는다.)

 

그리고 `mySessionId` 라는 이름으로 세션 ID 만 쿠키에 담아서 전달하고,

클라이언트는 쿠키 저장소에 `mySessionId` 쿠키를 보관한다.

 

쿠키만 사용할 때의 문제점을 세션으로 해결

- 쿠키값은 클라이언트에서 서버로 전달하는 것이기 때문에 위변조가 가능하다.

=> 추정 불가능한 세션 ID 를 사용

- 쿠키에 보관된 정보는 보안이 되지 않는다.

=> 세션 ID 에는 중요한 정보가 없다.

- 해커가 쿠키를 탈취하면 평생 사용할 수 있다.

=> 서버에서 세션의 만료시간을 짧게 유지 or 해킹이 의심되는 경우 세션을 강제 제거

 

세션 정보

`seesionId` : 세션 ID, JSESSIONID 의 값

`maxInactiveInterval` : 세션의 유효 시간

`creationTime` : 세션 생성일시

`lastAccessedTime` : 최근에 서버에 접근한 시간, 클라이언트에서 서버로 sessionId 요청한 경우 갱신

`isNew` : 새로 생성된 세션인지, 클라이언트에서 서버로 sessionId 요청해서 조회된 세션인지 여부

 

타임아웃

세션은 사용자가 로그아웃을 직접 호출하여 `session.invalidate()` 가 호출되는 경우 삭제된다.

하지만 대부분 로그아웃 대신에 그냥 웹 브라우저를 종료한다. 

따라서 `HttpSession` 은 사용자가 서버에 최근에 요청한 시간을 기준으로 30분 정도를 유지해 주는 방식을 사용한다.

 

세션 타임아웃은 해당 옵션을 통해 설정할 수 있다.

 

글로벌 설정

application.properties
server.servlet.session.timeout=1800

 

특정 세션 단위로 설정

session.setMaxInactiveInterval(1800);

 

세션 직접 만들기

세션 저장소

@Component
public class SessionManager {
  public static final String SESSION_COOKIE_NAME = "mySessionId";
  private Map<String, Object> sessionStore = new ConcurrentHashMap<>();

  public void createSession(Object value, HttpServletResponse response) {
    String sessionId = UUID.randomUUID().toString();
    sessionStore.put(sessionId, value);

    Cookie mySessionCookie = new Cookie(SESSION_COOKIE_NAME, sessionId);
    response.addCookie(mySessionCookie);
  }
  
  public Object getSession(HttpServletRequest request) {
    Cookie sessionCookie = findCookie(request, SESSION_COOKIE_NAME);
    if (sessionCookie == null) {
      return null;
    }
    return sessionStore.get(sessionCookie.getValue());
  }

  public void expire(HttpServletRequest request) {
    Cookie sessionCookie = findCookie(request, SESSION_COOKIE_NAME);
    if (sessionCookie != null) {
      sessionStore.remove(sessionCookie.getValue());
    }
  }
  
  private Cookie findCookie(HttpServletRequest request, String cookieName) {
    if (request.getCookies() == null) {
      return null;
    }
    return Arrays.stream(request.getCookies())
                 .filter(cookie -> cookie.getName().equals(cookieName))
                 .findAny()
                 .orElse(null);
  }
}

`HashMap` 은 동시 요청에 안전하지 않아, 안전한 `ConcurrentHashMap` 을 사용했다.

 

> 참고 <

`HttpServletRequest`, `HttpServletResponse` 객체를 직접 테스트할 수 없기 때문에

테스트할 때는 가짜 객체인 `MockHttpServletRequest` 와 `MockHttpServletResponse` 를 사용한다.

 

로그인

@PostMapping("/login")
public String login(@Valid @ModelAttribute LoginForm form,
                    BindingResult bindingResult, HttpServletResponse response) {
  ...
  sessionManager.createSession(loginMember, response);

  return "redirect:/";
}
@GetMapping("/")
public String homeLogin(HttpServletRequest request, Model model) {
  Member loginMember = (Member) sessionManager.getSession(request);
  if (loginMember == null) {
    return "home";
  }
  
  model.addAttribute("member", loginMember);
  return "loginHome";
}

 

로그아웃

@PostMapping("/logout")
public String logout(HttpServletRequest request) {
  sessionManager.expire(request);
  return "redirect:/";
}

 

서블릿 HTTP 세션

HttpSession 

서블릿이 제공하는 HttpSession 은 쿠키 이름이 `JSESSIONID` 이고, 값은 추정 불가능한 값이다.

 

- `request.getSession(true)` (기본값, 생략 가능)

  : 세션이 있으면 기존 세션 반환

  : 세션이 없으면 새로운 세션을 생성해서 반환

- `request.getSession(false)`

  : 세션이 있으면 기존 세션 반환

  : 세션이 없으면 null 반환

 

로그인

@PostMapping("/login")
public String login(@Valid @ModelAttribute LoginForm form,
                    BindingResult bindingResult, HttpServletRequest request) {
  ...
  HttpSession session = request.getSession();
  session.setAttribute(SessionConst.LOGIN_MEMBER, loginMember);

  return "redirect:/";
}
@GetMapping("/")
public String homeLogin(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";
}

홈 로그인에서, 세션을 찾지 못하면 null 로 다시 홈 화면으로 보내야 하므로

request.getSession(`false`) 를 한다.

 

로그아웃

@PostMapping("/logout")
public String logout(HttpServletRequest request) {
  HttpSession session = request.getSession(false);
  if (session != null) {
    session.invalidate();
  }
  return "redirect:/";
}

`session.invalidate()` : 세션을 제거한다.

 

어차피 로그아웃을 할 것이기 때문에 세션을 찾지 못해도 의미 없는 세션을 만들면 안 되므로,

request.getSession(`false`) 를 한다.

 

@SessionAttribute

이미 로그인된 사용자를 찾을 때 사용할 수 있다. 새로운 세션을 생성하지 않는다.

@GetMapping("/")
// public String homeLogin(HttpServletRequest request, Model model) {
public String homeLogin(@SessionAttribute(name = SessionConst.LOGIN_MEMBER, required = false) 
                        Member loginMember, 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";
}

세션을 찾고, 세션에 들어있는 데이터를 찾는 과정을 스프링이 편리하게 처리해 준다.

 

TrackingModes

로그인을 처음 시도하면 URL 에 `jsessionid` 를 포함하고 있다.

 

이것은 웹 브라우저가 쿠키를 지원하지 않을 때 쿠키 대신 URL 을 통해 세션을 유지하는 방법이다.

따라서 URL 에 이 값을 계속 포함해서 전달해야 하므로 현실성이 떨어진다.

 

그래도 존재하는 이유는, 서버 입장에서 웹 브라우저가 쿠키를 지원하는지 최초에는 판단하지 못하므로,

쿠키 값과 URL 에 jsessionid 를 함께 전달한다.

 

URL 전달 방식을 끄고 싶으면 해당 옵션을 넣으면 된다.

application.properties
server.servlet.session.tracking-modes=cookie

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

출처 | 스프링 MVC 2(김영한) - 인프런

728x90