쿠키와 세션을 이용한 간단한 로그인을 구현해보자! 우선 쿠키와 세션이 무엇인지에 대해 간단하게 알아보자.
쿠키란 웹을 사용하는 유저들에게 편리함을 제공하거나, 이를 이용해 정보를 수집할 수 있는 브라우저에 저장하게 되는 작은 텍스트이다.
예를 들어 'Hello'라는 ID를 가지는 유저가 로그인후 잠시 사이트를 닫은후 다시 재접속하여도 로그인 상태를 유지하게끔 하는데, 이는 바로 쿠키를 이용한 것이다.
웹사이트에 접속하면 브라우저는 서버로 요청을 보내는데 서버에서는 해당 요청에 대한 응답에 쿠키를 만들어 브라우저로 보낸다. 해당 브라우저는 응답받은 쿠키를 쿠키저장소에 보관하게 되며, 이후 다시 유저가 웹사이트에 접속하게 되면 쿠키저장소에서 이전에 서버에서 발급해준 쿠키정보를 서버로 요청을 보내게 되는데, 서버에서는 이 쿠키값을 확인하여 재접속이더라도 계속 로그인 상태를 유지할수 있게끔 한다.
세션이란 쿠키의 보안 이슈에 대해 이를 방지하고자 나온 개념이다. 쿠키만을 이용해서 로그인 또는 사용자를 식별하려면 매우 심각한 보안이슈가 일어난다. 쿠키에는 다음과 같은 여러가지 보안이슈가 있다.
위와 같이 쿠키는 직접 추가, 변경이 가능할 뿐더러 해커가 탈취해가기도 매우 쉬운 환경에 놓여있다. 만약 사용자의 민감한 개인정보를 쿠키에 담을경우 심각한 보안문제가 발생할 것이다. 세션은 이를 위한 대안이다.
이와 같이 사용자의 중요한 정보는 서버에서 관리하고 임의의 토큰(랜덤값)을 쿠키로 만들어 브라우저에게 응답후, 이 쿠키값을 가지고 클라이언트와 서버의 연결을 유지하는 방법을 세션이라고 한다.
여기까지 쿠키와 세션을 간단히 알아보았고 이제 구현으로 들어가보자.
직접 Session을 관리하는 SessionManager와 Servlet에서 제공하는 Session을 사용하여 두가지 방식으로 구현을 해볼 것이다. 해당 구현의 소스코드는 https://github.com/gwjeondev/loginTest 에서 확인할 수 있다!
디렉토리는 다음과 같다.
localhost:8080 접속시 로그인 페이지가 나온다. 로그인 ID와 비밀번호를 입력하여 접속하자. 회원 정보는 사전에 등록해두었다. 전송버튼 클릭시 아래 Controller로 요청된다.
@PostMapping("/login")
public String login(@ModelAttribute Member member, HttpServletResponse response) {
Member loginMember = loginService.login(member.getMemberId(), member.getPassword()); //(1)
if(loginMember == null) { //(2)
return "redirect:/";
}
sessionManager.createSession(loginMember.getMemberId(), response); //(3)
return "redirect:/"; //(4)
}
(1): 입력된 회원ID와 Password를 가지는 회원이 있는지 확인한다. 올바른 정보인경우 Member객체가 반환되고, 아니라면 null이 반환된다.
(2): null일 경우 올바른 정보가 입력되지 않았으므로 /로 redirect 시킨다.
(3): 요청받은 정보가 올바른 회원정보임이 확인되었으니, 세션을 생성한다. MemberId와 response를 argument로 sessionManager.createSession를 호출한다.
public void createSession(String value, HttpServletResponse response) {
String token = UUID.randomUUID().toString(); //(1)
store.put(token, value); //(2)
Cookie cookie = new Cookie(SessionConst.sessionId, token); //(3)
response.addCookie(cookie); //(4)
}
public interface SessionConst {
String sessionId = "LOGIN_MEMBER";
}
(4): 정상적으로 세션저장소에 회원정보를 등록하고, response에도 쿠키에 세션정보를 실어 등록하였으므로 /으로 redirect한다.
@GetMapping("/")
public String home(HttpServletRequest request, Model model) {
String memberId = sessionManager.getSession(request); //(1)
if(memberId == null) { //(2)
return "login";
}
Optional<Member> findMemberOptional = memberRepository.findByMemberId(memberId); //(3)
Member member = findMemberOptional.orElse(null); //(4)
if(member == null) { //(4)
return "login";
}
model.addAttribute("member", member); //(5)
return "home"; //(5)
}
URL /요청을 처리하는 Controller이다.
(1): 세션정보를 가져오기 위하여 request를 argument로 sessionManager.getSession를 호출한다.
public String getSession(HttpServletRequest request) {
Cookie sessionCookie = findCookie(request); //(1)
if(sessionCookie == null) { //(2)
return null;
}
return store.get(sessionCookie.getValue()); //(3)
}
(1): request요청의 Cookie에 서버에서 관리되는 쿠키정보가 있는지 찾는다. reqeust를 argument로 findCookie를 호출한다.
public Cookie findCookie(HttpServletRequest request) {
//request 요청에 cookie가 없을 경우 null
if(request.getCookies() == null) { //(1)
return null;
}
return Arrays.stream(request.getCookies()) //(2)
.filter(cookie -> cookie.getName().equals(SessionConst.sessionId))
.findFirst()
.orElse(null);
}
SessionConst.sessionId
즉 LOGIN_MEMBER
가 있는지 찾고 있다면 해당 Cookie를 return 한다.(2): (1)에서 유효한 쿠키를 찾지못하고 return 받은 sessionCookie가 null이라면 null을 return 한다.
(3): 유효한 쿠키를 찾았을 경우 Cookie의 Value(token값)을 통하여 Store Map(세션저장소)에서 memberId를 찾아온다.
(2): (1)에서 유효한 Session을 찾지 못한 경우 mebmerId는 null이 되며, login 페이지로 이동시킨다.
(3): (1)에서 유효한 Session을 통해 정상적인 memberId를 반환받았을 경우 memberRepository에서 member정보를 찾는다.
(4): memberId를 통하여 member를 찾지 못했을 경우 member는 null이 되며, login 페이지로 이동시킨다.
(5): model에 member를 추가하여 home 페이지로 이동시킨다.
아래는 모든 과정이 정상적으로 마무리되고, home페이지로 이동한 화면이다.
정상적으로 login된 home화면을 확인할 수 있다.
F12 개발자모드로 접속해 등록된 Cookie를 확인해보자! 앞으로 다시 사이트에 접속하더라도 이 Cookie에 등록된 임의의 Token값을 통해 로그인상태를 유지할 것이다.
로그아웃을 통해 서버의 세션저장소에 등록된 정보를 삭제해보자.
home 화면에서 로그아웃 버튼을 클릭하면 /logout url로 요청된다.
@PostMapping("logout")
public String logout(HttpServletRequest request) {
sessionManager.sessionExpire(request); //(1)
return "redirect:/"; //(2)
}
(1): 세션정보를 삭제하기 위하여 request를 argument로 sessionManager.sessionExpire를 호출한다.
public void sessionExpire(HttpServletRequest request) {
Cookie sessionCookie = findCookie(request); //(1)
if(sessionCookie != null) { //(2)
store.remove(sessionCookie.getValue()); //(2)
}
}
(2): /으로 redirect 한다. /요청을 처리하는 Controller에서는 다시 reqeust요청에 Cookie가 있는지 확인후 세션이 삭제되었으므로 최종적으로 login페이지로 이동할 것이다.
지금까지 SessionManager를 직접 만들어 처리하는 방식에 대해 알아보았고 로그아웃이 정상적으로 진행되어 세션정보가 삭제되었다면 다시 localhost:8080으로 접속하여도 로그인상태를 유지에 실패할 것이다.
지금까지 성공적으로 쿠키와 세션을 이용하여 로그인을 구현하였는데, 한가지 문제점이 있다. 세션정보를 서버에서 삭제하는 일은 사용자가 로그아웃 버튼을 눌렀을때만 발생한다. 일반적으로 사용자가 웹사이트를 떠날때 브라우저를 종료하지 로그아웃 버튼을 직접 클릭하는 경우는 드물다. 이렇게 된다면 해당 사용자에 대한 세션정보는 서버에 계속 유지될것이고 또한 만약 해커가 브라우저의 쿠키에서 세션Key를 탈취할 경우 보안문제로 이어질수 있다.
즉 이러한 문제를 해결하기 위해선 시간 간격을 두어 얼마의 시간만큼 해당 세션정보를 가지는 request 요청이 없을 경우, 서버의 세션정보를 지워야한다. 예시로 A사용자가 30분간 웹사이트를 이용하지 않는 경우 서버의 세션정보를 삭제 해버리는 것이다. 말로 들었을땐 어려워 보이는 구현이지만 Servlet에서 제공하는 Session기능을 이용할 경우 매우 편리하게 구현할 수 있다. 이뿐만 아니라 지금까지 SessionManager를 통해 구현했던 쿠키와 세션기능을 코드 몇줄로 매우 편리하게 축약할 수 있다.
지금부터 Servlet이 제공하는 Session을 알아보자.
localhost:8080 접속시 로그인 페이지가 나온다. 로그인 ID와 비밀번호를 입력하여 접속하자. 회원 정보는 사전에 등록해두었다. 전송버튼 클릭시 아래 Controller로 요청된다.
Controller를 확인하기 전에 reqeust.getSession()이라는 메서드가 사용될것인데, 어떠한 기능을 수행하는지 정리하고 가자. request.getSession()는 세션을 생성한다.
@PostMapping("/login")
public String login(@ModelAttribute Member member, HttpServletRequest request) {
Member loginMember = loginService.login(member.getMemberId(), member.getPassword()); //(1)
if(loginMember == null) { //(2)
return "redirect:/"; //(2)
}
HttpSession session = request.getSession(); //(3)
session.setAttribute(SessionConst.sessionId, loginMember.getMemberId()); //(4)
return "redirect:/"; //(4)
}
SessionConst.sessionId
즉 LOGIN_MEMBER
와 사용자의 memberId를 저장한후 /으로 redirect 한다.
앞에서는 SessionManager에서 Cookie를 직접 new로 생성하여 response.addCookie()를 통해 Cookie정보를 넘겨줬다면 Servlet에서 제공하는 Session은 3번 4번의 과정이 이루어지게 될 경우 Servlet이 자체적으로 Cookie에 정보를 넘겨준다. F12 개발자모드로 쿠키를 확인해보자. JSESSIONID
는 Servlet에서 default로 넣어주는 Name이다.
그리고 지금까지 문제없이 진행됐다면 아마 url의 주소를 봤을때 http://localhost:8080/;jsessionid=855423ECD5AF593C588A9ED7ABF55CFD 라고 입력이 되어있을 것이다. 자세히 보면 jsessionid=855423ECD5AF593C588A9ED7ABF55CFD는 서버에서 발급해준 Cookie와 같다. 이는 Servlet에서 Cookie를 사용하지 못하는 환경에 대비해(브라우저가 쿠키를 사용하지 못하거나, 기타 다른 이유로 인하여)자동으로 url에 해당 정보를 실어주는데 일반적으로 쿠키를 사용하지 못하는 환경은 없다고 봐도 무방하니.. 해당 기능을 꺼주도록 하자. 스프링부트에서는 간단하게 해당 기능을 제공한다. application.properties에 다음내용을 추가하자.
server.servlet.session.tracking-modes=cookie
위 내용을 추가후 다시 서버를 시작하고 접속하면 정상적으로 url이 나오는것을 확인할 수 있다.
이제 /으로 redirect 시켰으니 /의 요청을 처리하는 Controller를 확인해보자.
@GetMapping("/")
public String home(HttpServletRequest request, Model model) {
HttpSession session = request.getSession(false); //(1)
if(session == null) { //(2)
return "login"; //(2)
}
String memberId = (String)session.getAttribute(SessionConst.sessionId); //(3)
Optional<Member> findMemberOptional = memberRepository.findByMemberId(memberId); //(4)
Member member = findMemberOptional.orElse(null); //(5)
if(member == null) { //(5)
return "login"; //(5)
}
model.addAttribute("member", member); //(6)
return "home"; //(6)
}
SessionConst.sessionId
즉 LOGIN_MEMBER
를 Attribute로 등록했던 mebmerId를 찾는다.아래는 모든 과정이 정상적으로 마무리되고, home페이지로 이동한 화면이다.
정상적으로 login된 home화면을 확인할 수 있다.
로그아웃을 통해 서버의 세션저장소에 등록된 정보를 삭제해보자.
home 화면에서 로그아웃 버튼을 클릭하면 /logout url로 요청된다.
@PostMapping("/logout")
public String logout(HttpServletRequest request) {
HttpSession session = request.getSession(false); //(1)
if(session == null) { //(2)
return "redirect:/"; //(2)
}
session.invalidate(); //(3)
return "redirect:/"; //(3)
}
스프링부트에서는 간단하게 해당기능을 제공한다. application.properties에 다음 내용을 추가하자.
server.servlet.session.timeout=1800
1800초(30분)라는 의미이다.
위 내용을 추가하게 되면 30분간 Session정보를 가지는 request 요청이 없을경우 자동으로 서버에서 세션을 삭제한다.