스프링의 Bean Validation에 대해 학습하였다. Bean Validation 은 @ModelAttribute 로 Form 데이터를 받아오는 객체에 어노테이션을 추가함으로서 검증을 수행하는 매우 강력한 검증 방식이다. 하지만 @RequestBody 로 JSON을 객체로 받아오는 경우 타입에러가 나면 검증을 수행할 수 없는 한계가 있다.
그러면 이번 포스팅에서는 Stateless 프로토콜인 HTTP에서 로그인을 유지할 수 있는 쿠키와 세션에 대해 알아보자.
LoginController 의 login 부분이다. 로그인 실패시 reject 메서드로 ObejectError를 bindingResult에 추가하고 loginForm 으로 Redirect 한다. 그러면 로그인 성공 시 로직은 어떻게 짜야 로그인을 유지할 수 있을까?
쿠키란 서버가 클라이언트에 전송하는 데이터 조각이다. 쿠키는 클라이언트에 저장되며 클라이언트가 모든 Request를 보낼 때 마다 쿠키를 추가하여 전송하게 된다. 서버는 쿠키를 확인하여 클라이언트를 인식하고 처리를 수행한다.
영속 쿠키 : 만료 날짜를 입력하면 해당 날짜까지 유지됨
세션 쿠키 : 만료 날짜를 생략하면 브라우저 종료시까지만 유지됨
LoginController 에서 로그인 성공시 new Cookie 로 쿠키를 추가한 뒤 response.addCookie 로 쿠키를 응답에 추가하여 사용자에게 반환하도록 하였다. 쿠키 값은 String 만 가능하여 Long 인 memberId를 String으로 변환하여 쿠키를 생성하였다.
HomeController 이다. @CookieValue를 통해 쿠키 값을 가져오고, 존재한다면 쿠키 값인 memberId로 Member 객체를 찾아 뷰로 전송한다. 뷰에서는 로그인 유저의 Member 객체를 활용해 뷰를 그린다.
로그아웃 요청시 expireCookie메서드로 쿠키 유효기간을 0으로 만들어 만료시키고 응답에 추가한다.
우리는 쿠키에 로그인한 유저의 memberId를 저장하여 넘겨주고. 이를 바탕으로 Member를 식별할 수 있었다. 그런데 이러한 쿠키방식 로그인은 쿠키를 클라이언트가 저장하기에 심각한 보안문제가 존재한다.
이를 보완하기 위해 쿠키에 랜덤 토큰 값을 넣고. 서버에서 토큰 값으로 memberId 를 식별하여야 한다. 또한 해커가 토큰을 털어가도 사용할 수 없도록 토큰 만료시간을 짧게 설정해야한다. 이를 바탕으로 쿠키 + 세션으로 로그인을 구현해보자.
세션을 생성하는 메서드이다. 새로운 UUID로 sessionId 를 생성하고 sessionId:Object를 세션저장소에 삽입한다. 그 후 meySessionId:sessionId 로 쿠키를 생성하여 클라이언트에게 반환한다.
클라이언트의 쿠키의 sessionId로 세션을 조회하여 객체를 가져오는 메서드이다. request.getCookies 는 쿠키 배열을 반환하므로 여기서 특정이름의 쿠키를 찾는다. 쿠키에서 sessionId를 가져오고, sessionId로 세션저장소에서 객체를 찾아 반환한다.
request 에서 mySessionId 라는 쿠키를 찾아 sessionId를 가져오고 해당 sessionId 를 세션저장소에서 삭제한다.
LoginController 에서 로그인 성공시 세션을 생성하고
Member 객체를 찾아 뷰로 넘겨줄 HomeController 에서 세션 조회를 수행하여 Member 객체를 찾아 사용하고
로그아웃 시에는 세션을 만료시키도록 하였다.
세션을 이용한 로그인을 구현해보았는데, 식별은 쿠키의 UUID를 통해 진행하며. 세션은 정보가 없는 UUID에 매핑되는 객체 정보를 서버에서 저장하는 것 뿐이다.
세션이라는 것은 stateless 한 HTTP를 극복하기 위해 필수적 요소로서. 초창기인 서블릿부터 HttpSession 이라는 기능을 제공한다.
서블릿이 제공하는 HttpSession 도 직접 만든 세션과 유사하게 동작한다. 다만 세션 등록시 생성하는 쿠키이름이 JSESSIONID 이고, 랜덤한 값을 갖는다. 이를 바탕으로 클라이언트마다 세션 객체를 할당해준다.
로그인 성공시 세션을 가져온다. 이 때 getSession 을 사용하는데 서버에 세션이 존재한다면 해당 세션을 가져오고, 존재하지 않는다면 세션을 새로 생성하여 가져온다. 파라미터를 false로 하면 존재하지 않을 경우 세션을 새로 생성하지 않도록 할 수 있다. 세션은 클라이언트 마다 생성하며, 하나의 세션에 여러 데이터를 저장할 수 있다. 이때 세션에 "loginMember" 로 loginMember 객체를 저장하였다.
세션에서 Memeber 객체 찾는 HomeController 부분이다. 우선 클라이언트의 세션을 조회하고, 세션에 loginMember 에 매핑되는 객체가 존재한다면 이를 가져와 뷰 렌더링에 사용한다.
로그아웃 요청시 세션을 가져온 뒤 session.invalidate()로 세션을 만료시킨다.
즉 HttpSession 은 getSession 시 클라이언트가 JSESSIONID의 쿠키가 없다면 독자적인 JSESSIONID를 갖는 세션을 생성해 관리해준다. JSESSIONID 쿠키의 발급을 통해 클라이언트마다 독자적인 세션객체를 갖게된다.
@SessionAttribute. 스프링은 세션을 더 편리하게 사용하도록 @SessionAttribute를 지원한다.
HomeController 부분이다. getSession + session.getAttribute 를 한번에 수행해준다. 추가로 해당 기능은 세션을 생성하지 않는다.
application.properties 에 해당 부분을 추가하자. 웹 브라우저가 쿠키를 사용하지 않을 때 URL 에 데이터를 담아 전송하는 것을 방지하며, 세션 유지시간을 지정한다. 1800 으로 설정시 세션 마지막 요청 기준 30분 후 세션이 제거된다. 세션 제거시간을 지정하지 않는다면, 클라이언트 사용자마다 세션을 생성하는 특성상 서버의 메모리가 터지는 현상이 발생할 것이다.