2023.07.28 - [Http] - HTTP Header(헤더)의 구조, 쿠키
우선, 쿠키가 어떻게 동작하는지 알아야 하기 때문에, 위의 글을 읽어보고 해당 글을 읽는 것을 추천한다.
현 상황은 웹 사이트를 만드는 중인데, 로그인을 하는 로직 등과 검증 로직 등을 작성한 상태이다.
쿠키, 세션을 사용해서 로그인, 로그아웃 기능을 구현해보는 것을 목표로 한다.
로그인 상태 유지
로그인의 상태를 어떻게 유지할 수 있을까?
쿼리 파라미터를 계속 유지하면서 보내는 것은 어려운 일이다.
이런 상황에 쿠키를 사용해보자.
쿠키의 종류
- 영속 쿠키 : 만료 날짜를 입력하면, 해당 날짜까지 유지
- 세션 쿠키 : 만료 날짜를 생략하면, 브라우저 종료시까지 유지
본인은 브라우저의 종료와 함께 로그아웃이 되는 걸 기대하므로, 세션 쿠키를 사용하도록 하겠다.
@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:/";
}
쿠키 생성 로직
→ 로그인에 성공하면 쿠키를 생성, HttpServletResponse에 담는다. 쿠키 이름은 memberId, 값은 회원의 id를 담는다.
웹 브라우저는 종료 전까지 회원의 id를 서버에 계속 보낼 것이다.
@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";
}
HomeController의 코드이다.
@CookieValue를 사용해서 편리하게 쿠키를 조회
로그인한 사용자에게는 "loginHome"을 보여주고, 비로그인자는 "home"으로 보낸다.
로그아웃
@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으로 지정한다.
로그아웃 기능도 응답 쿠키를 생성하는데, 이는 Max-Age가 0으로 지정되어, 바로 종료된다.
쿠키와 보안 문제
지금까지의 코드에는 심각한 보안 문제가 발생한다.
실제 웹 브라우저에서 개발자 모드를 통해서, Cookie의 값을 변경할 수 있다.
해당 쿠키의 값을 변경하면 사용자의 정보가 변경되며, 다른 사용자가 될 수 있다.
때문에, 해커가 해당 쿠키를 훔쳐간다면, 이를 통해 악의적인 요청을 계속 시도할 수 있다.
그렇다면, 이를 막는 대안은 뭘까?
세션을 사용하자
세션
위의 보안 문제를 해결하기 위해선, 결국 사용자의 중요한 정보를 모두 서버에 저장을 해야 한다.
그리고, 클라이언트와 서버는 추정 불가능한 임의의 식별자 값으로 연결해야 한다.
(ex. 추정 가능한 식별자 (memberId = 1) : 1을 2,3,4...로 바꿔가면서 다른 사용자로 변경할 시도가 가능)
이를 다 세션을 통해 해결할 수 있다.
세션 ID는 uuid로 만들어지기 때문에, 추정이 불가능하다.
해당 세션 ID와 보관할 값은 서버의 세션 저장소에 보관한다.
서버는 mySessionId라는 이름으로 세션id만 쿠키에 담아서 전달
클라이언트는 쿠키 저장소에 mySessionId 쿠키를 보관
결론적으로, 회원과 관련된 중요 정보는 클라이언트로 전달하지 않게 된다.
이후로 클라이언트는 요청시마다 항상 mySessionId 쿠키를 전달하게 된다.
세션을 사용해서 서버에서 중요한 정보를 관리하고, 보안 문제를 해결할 수 있다.
세션 적용하기-로그인
-세션 생성 (sessionId 생성, 세션 저장소에 sessionId와 보관할 값 저장, 응답 쿠키 생성해서 전달)
-세션 조회 (요청받은 sessionId 쿠키 값으로 세션 저장소에서 값 조회)
-세션 만료 (요청한 sessionId 쿠키의 값으로, 세션 저장소에 있는 sessionId와 값 제거)
이렇게 크게 3가지를 개발해서 직접 세션을 만들어서 로그인 기능에 적용할 수 있다.
그러나, 우리는 서블릿이 공식 지원하는 세션을 사용해서 로그인 기능에 적용시키도록 하겠다.
HttpSession
서블릿을 통해 HttpSession 을 생성하면 다음과 같은 쿠키를 생성한다.
쿠키 이름이 JSESSIONID 이고, 값은 추정 불가능한 랜덤 값이다.
Cookie: JSESSIONID=5B78E23B513F50164D6FDD8C97B0AD05
HttpSession을 사용해서 코드를 작성해보자.
public class SessionConst {
public static final String LOGIN_MEMBER = "loginMember";
}
HttpSession에 데이터를 보관,조회할 때, 같은 이름이 중복으로 사용되므로, 상수로 정의하였다.
<LoginController>
//세션이 있으면 있는 세션 반환, 없으면 신규 세션 생성
HttpSession session = request.getSession();
//세션에 로그인 회원 정보 보관
session.setAttribute(SessionConst.LOGIN_MEMBER, loginMember);
return "redirect:/";
@PostMapping()안에 HttpSession을 적용한 코드이다.
getSession()을 통해서 세션을 반환할 수 있다.
이는 (true)가 default값으로 세션을 찾았을 때, 없으면 새로 세션을 생성한다.
만일, getSession(false)로 지정한다면, 새로 세션을 생성하지 않는다.
로그아웃
@PostMapping("/logout")
public String logoutV3(HttpServletRequest request) {
//세션을 삭제한다.
HttpSession session = request.getSession(false);
if (session != null) {
session.invalidate();
}
return "redirect:/";
}
우선, getSession(false)로 지정한다. ( 세션을 조회하는데, 새로 또 생성하면 안되기 때문에 )
해당 세션에 값이 존재한다면, session.invalidate()를 통해 세션을 삭제한다.
<HomeController>
@GetMapping("/")
public String homeLogin(
@SessionAttribute(name = SessionConst.LOGIN_MEMBER, required = false)
Member loginMember,
Model model) {
//세션에 회원 데이터가 없으면 home
if (loginMember == null) {
return "home";
}
//세션이 유지되면 로그인으로 이동
model.addAttribute("member", loginMember);
return "loginHome";
}
@SessionAttribute(name="",required=_)를 통해서 세션을 더 편리하게 사용할 수 있다.
이미 로그인된 사용자를 찾을 때,
@SessionAttribute(name = SessionConst.LOGIN_MEMBER, required = false) Member loginMember
이렇게 작성해주면 된다.
required는 false로 되어있으므로, 세션을 새로 생성하지 않는다.
TrackingModes
로그인을 처음 시도하면, URL이 다음과 같이 jsessionid를 포함한다.
http://localhost:8080/;jsessionid=F59911518B921DF62D09F0DF8F83F872
이것은 웹 브라우저가 쿠키를 지원하지 않을 때 쿠키 대신 URL을 통해서 세션을 유지하는 방법이다.
서버 입장에서 웹 브라우저가 쿠키를 지원하는지 하지 않는지 최초에는 판단하지 못하므로, 쿠키 값도 전달하고, URL에 jsessionid 도 함께 전달한다.
이를 URL에 보여주지 않고, 쿠키를 통해서만 세션을 유지하고 싶다면,
application,properties 에 아래의 옵션을 넣어주면 된다.
server.servlet.session.tracking-modes=cookie
HttpSession을 통해 세션에 대한 많은 정보를 확인할 수 있다.
sessionId, maxInactiveInterval, createTime, lastAccessedTime, isNew 등등이 예시이다.
확인해보고 싶으면 해당 코드를 통해 직접 확인해보자.
위의 예시 중에 lastAccessedTime을 통해서 세션의 타임아웃 발생 설정을 할 수 있다.
사용자는 보통 로그아웃을 누른 뒤, 창을 닫는 일보단 그냥 창을 닫는 것이 일반적이다.
그런데, 창을 닫는다고 해서, 이 사용자가 이용을 그만하고자 하는지 서버에서는 알 수 없다.
그래서, 마지막으로 해당 서버에 접근하는 시간을 기준으로 예를 들어, 30분동안 이용이 없다고 하면, 세션을 삭제하는 것이다.
session.setMaxInactiveInterval(1800); //1800초
시간 설정을 할 때는 초가 기본 단위이다.
글이 길었는데, 잘 정리가 되었으면 좋겠다.
다음 시간에는 서블릿 필터(Survlet Filter) 와 스프링 인터셉터(Spring Interceptor)에 대해서 알아보도록 하겠다.
'Spring' 카테고리의 다른 글
Spring - ArgumentResolver 활용 (0) | 2023.09.26 |
---|---|
Spring- 필터, 인터셉터(Filter, Interceptor) (0) | 2023.09.18 |
컴포넌트 스캔 (@ComponentScan) (0) | 2023.08.02 |
싱글톤 패턴, 싱글톤 컨테이너 (Singleton) (0) | 2023.08.02 |
스프링 컨테이너, 스프링 빈(Spring Container, Spring Bean) (0) | 2023.08.02 |