쿠키와 세션을 이용한 자동 로그인 방식에 대해서 정리해 보겠습니다.
[ 1. 쿠키와 세션이란? ]
: 쿠키와 세션은 매우 유사하면서도 다른 특징을 지니고 있는데요.
- 공통점 : 사용자의 정보(데이터)를 저장할 때 이용된다.
- 차이점 :
- 쿠키 : 1) 사용자의 로컬에 저장되었다가 브라우저가 요청시 왔다갔다하게 됨(보안에 취약)
2) 세션과 달리 여러 서버로 전송이 가능함
3) 세션이 브라우저 단위로 생성되어 브라우저 종료시 사라지는데 반해, 쿠키는 유효시간 설정을 할 수 있음. ex) 7일
- 세션 : 1) 서버에 데이터를 저장하여 쿠키에 비해 보안에 안전함
2) 브라우저 단위로 생성됨 => 익스플로러를 켜고 크롬을 켜고 하면 각각 2개의 세션이 생성되는 것
[ 2. why 쿠키와 세션을 이용한 로그인 처리를 하게 될까? ]
: 세션은 위에서 설명한대로 기본 단위가 "웹 브라우저"입니다. 따라서, 웹 브라우저 종료시 소멸하게 되죠...
그에 반해 쿠키는 사용자 PC에 저장되기 때문에 서버 요청시 전달되는 동안 네트워크 상에서 보안상 취약할 수는 있지만 유효시간을
길게 설정할 수 있어 브라우저가 종료되는 것과 별개로 7일 30일 등 기간을 길게 설정할 수 있습니다.
하지만,
그렇다고 쿠키에 로그인할 사용자의 정보를 담고 있는다면 정말 정말 너무 너무 보안상 취약할 것을 알 수 있겠죠?
따라서, 자동 로그인을 구현할 때에는 "< 세션과 쿠키를 동시에 사용하는 것 >"이 바람직하다고 생각합니다.
[ 3. 세션과 쿠키를 이용한 자동 로그인 구현에 대한 개요 ]
: 사용자가 로그인 폼에서 로그인을 할 당시, 자동로그인을 설정하겠다는 CheckBox를 클릭할 경우 사용자의 정보를 저장시키고 유효
기간을 설정한다는 것 까지는 알겠는데 그럼 도대체 어떤 사용자의 정보를 저장시켜 놓아야할까요?
먼저, 사용자가 로그인에 성공한 경우! -> 세션에 사용자 객체(UserVO)를 저장시켰었는데 앞에서 이 객체를 쿠키에 저장시킨다면, 굉장히 보안상 취약합니다. 비밀번호, 아이디 그 외 정보까지 UserVO에 들어 있었죠...
따라서, 로그인에 성공했을 때 사용자 DB 테이블에 sessionId와 유효시간 속성에 값을 지정하는 겁니다. 그리고 쿠키에는 세션Id를
넣어 놓는거죠... 그리고 "인터셉터"에서 해당 쿠키값이 존재하면 사용자 DB 테이블 내에서 유효시간 > now() 즉, 유효시간이 아직
남아 있으면서 해당 세션 Id를 가지고 있는 사용자 정보를 검색해 해당 사용자 객체를 반환하는 겁니다.
당연히, 쿠키가 유효시간이 다되면 해당 자동완성 기능은 동작하지 않게 되고 다시 쿠키를 사용하겠다는 선택을 했을 때 동작하게 되겠죠
그럼, 다음으로 코드상에서 직접 한번 알아 봅시다.
[ 4. 자동 로그인 실재로 구현해보기 ]
이번 장의 예제는 앞 게시글을 다 수행했다는 가정하에서 진행됩니다.
1) 먼저, UserController에서 로그인에 성공했으면서 사용자가 쿠키 사용 여부를 체크한 경우 -> 쿠키를 생성하고 세팅합시다.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 | package org.zerock.controller; import javax.inject.Inject; import javax.servlet.http.Cookie; import javax.servlet.http.HttpServletResponse; import javax.servlet.http.HttpSession; import org.springframework.stereotype.Controller; import org.springframework.ui.Model; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMethod; import org.zerock.domain.UserVO; import org.zerock.service.UserService; @Controller public class UserController { @Inject // byType으로 자동 주입 UserService service; // 로그인 폼을 띄우는 부분 @RequestMapping (value= "/login" ,method=RequestMethod.GET) public String loginForm(){ return "login/loginForm" ; // /login/loginForm.jsp를 띄움. } // end of loginForm // 로그인 처리하는 부분 @RequestMapping (value= "/loginProcess" ,method=RequestMethod.POST) public String loginProcess(HttpSession session,UserVO dto, HttpServletResponse response){ String returnURL = "" ; if ( session.getAttribute( "login" ) != null ){ // 기존에 login이란 세션 값이 존재한다면 session.removeAttribute( "login" ); // 기존값을 제거해 준다. } // 로그인이 성공하면 UserVO 객체를 반환함. UserVO vo = service.login(dto); if ( vo != null ){ // 로그인 성공 session.setAttribute( "login" , vo); // 세션에 login인이란 이름으로 UserVO 객체를 저장해 놈. returnURL = "redirect:/board/listPage" ; // 로그인 성공시 게시글 목록페이지로 바로 이동하도록 하고 /* * [ 세션 추가되는 부분 ] */ // 1. 로그인이 성공하면, 그 다음으로 로그인 폼에서 쿠키가 체크된 상태로 로그인 요청이 왔는지를 확인한다. if ( dto.isUseCookie() ){ // dto 클래스 안에 useCookie 항목에 폼에서 넘어온 쿠키사용 여부(true/false)가 들어있을 것임 // 쿠키 사용한다는게 체크되어 있으면... // 쿠키를 생성하고 현재 로그인되어 있을 때 생성되었던 세션의 id를 쿠키에 저장한다. Cookie cookie = new Cookie( "loginCookie" , session.getId()); // 쿠키를 찾을 경로를 컨텍스트 경로로 변경해 주고... cookie.setPath( "/" ); cookie.setMaxAge( 60 * 60 * 24 * 7 ); // 단위는 (초)임으로 7일정도로 유효시간을 설정해 준다. // 쿠키를 적용해 준다. response.addCookie(cookie); } } else { // 로그인에 실패한 경우 returnURL = "redirect:/login" ; // 로그인 폼으로 다시 가도록 함 } return returnURL; // 위에서 설정한 returnURL 을 반환해서 이동시킴 } // 로그아웃 하는 부분 @RequestMapping (value= "/logout" ) public String logout(HttpSession session) { session.invalidate(); // 세션 전체를 날려버림 // session.removeAttribute("login"); // 하나씩 하려면 이렇게 해도 됨. return "redirect:/board/listPage" ; // 로그아웃 후 게시글 목록으로 이동하도록...함 } } // end of controller |
코드를 살펴보면, service 객체의 login메서드를 통해 UserVO 객체를 반환하고 null이 아닌 경우 로그인에 성공했었죠?
이렇게 로그인에 성공되었으면서, 로그인 폼에서 checkBox를 선택한 경우(쿠키 사용하겠다는 체크박스) submit을 했을 때
UserVO 클래스 내의 useCookie 변수에 true/false로 값이 저장되어 들어 왔을 테니까
로그인에 성공했으면서 + 쿠키사용을 체크한 경우에 세션을 추가하도록 하는 부분이 앞에 코드에서 추가된 겁니다.
이때, 사용자 PC에서 쿠키를 보내는 경로가 "/" 로 설정함으로써 contextPath 이하의 모든 요청에 대해서 쿠키를 전송할 수 있
도록 설정한다는 것이고, 유효시간은 (초)단위 임으로 60 * 60 * 24 * 7로 세팅해주면, 로그인 후 해당 쿠키는 7일동안 유지될 수
있게 됩니다.(브라우저의 종료와 관계없이)
이때, 가장 중요하게 볼 부분이 쿠키에 UserVO 객체를 저장하는 것이 아니고!!!!(사실 쿠키는 문자열만 저장되기 때문에 가능하지도 않습니다.)
현재 브라우저의 세션 id를 저장해 놓는 겁니다.
그럼... 쿠키에 의해 자동로그인 기간은 제어가 될 것이고... 사용자는 해당 세션 id에 대한 정보를 가지고 있어야 겠죠??
따라서, 다음으로는 DB의 userTable에 세션Id와 유효시간 정보를 담을 수 있는 컬럼을 추가하도록 합시다.
2) DB userTable에 세션Id와 유효시간을 설정할 수 있는 컬럼을 만들기
1 2 | alter table userTable add column sessionkey varchar( 50 ) not null default 'none' ; alter table userTable add column sessionlimit timestamp; |
3) userMapper.xml에 작업을 합시다.
1. 로그인 성공시 sessionId와 유효시간을 저장하는 부분 작성
2. 사용자가 이전에 로그인에 성공했었는지 확인하는 부분
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 | <?xml version= "1.0" encoding= "UTF-8" ?> <!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" <mapper namespace= "org.zerock.mapper.UserMapper" > <!-- login에 대한 DB 작업을 정의한다. id와 pw가 일치하면 사용자 정보를 담고 있는 객체를 반환한다. --> <select id= "login" resultType= "UserVO" > select * from userTable where userId = #{userId} and userPw = #{userPw} </select> <!-- 로그인된 경우 해당 세션id와 유효시간을 사용자 테이블에 세팅한다. --> <update id= "keepLogin" > update userTable set sessionKey = #{sessionId}, sessionLimit = #{next} where userId=#{userId} </update> <!-- 유효기간이 남아 있으면서 해당 sessionId를 가지는 사용자 정보를 꺼내오는 부분 --> <select id= "checkUserWithSessionKey" resultType= "UserVO" > select * from userTable where sessionKey = #{sessionId} and sessionLimit > now() </select> </mapper> |
4) userDAO 인터페이스와 userDAOImpl 클래스를 수정합시다.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | package org.zerock.persistence; import java.sql.Date; import org.zerock.domain.UserVO; public interface UserDAO { public UserVO login(UserVO dto); // 자동로그인 체크한 경우에 사용자 테이블에 세션과 유효시간을 저장하기 위한 메서드 public void keepLogin(String uid, String sessionId, Date next); // 이전에 로그인한 적이 있는지, 즉 유효시간이 넘지 않은 세션을 가지고 있는지 체크한다. public UserVO checkUserWithSessionKey(String sessionId); } |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 | package org.zerock.persistence; import java.sql.Date; import java.util.HashMap; import java.util.Map; import javax.inject.Inject; import org.apache.ibatis.session.SqlSession; import org.springframework.stereotype.Repository; import org.zerock.domain.UserVO; @Repository public class UserDAOImpl implements UserDAO { @Inject SqlSession sqlSession; /** * login에 성공하면, 유저 정보를 담고 있는 UserVO 객체를 반환한다. */ @Override public UserVO login(UserVO dto) { // Mapper의 namespace명.id : 자신에게 맞게 작성해서 넣는다. return sqlSession.selectOne( "org.zerock.mapper.UserMapper.login" , dto); } // 자동로그인 체크한 경우에 사용자 테이블에 세션과 유효시간을 저장하기 위한 메서드 public void keepLogin(String uid, String sessionId, Date next){ Map<String, Object> map = new HashMap<String,Object>(); map.put( "userId" , uid); map.put( "sessionId" , sessionId); map.put( "next" , next); // Mapper.xml로 데이터를 전달할 때 한 객체밖에 전달 못함으로 map으로 묶어서 보내줌 단... 주의할 점은 // Mapper.xml 안에서 #{} 이 안에 지정한 이름이랑 같아야함.. 자동으로 매핑될 수 있도록 // 아래가 수행되면서, 사용자 테이블에 세션id와 유효시간이 저장됨 sqlSession.update( "org.zerock.mapper.UserMapper.keepLogin" ,map); } // 이전에 로그인한 적이 있는지, 즉 유효시간이 넘지 않은 세션을 가지고 있는지 체크한다. public UserVO checkUserWithSessionKey(String sessionId){ // 유효시간이 남아있고(>now()) 전달받은 세션 id와 일치하는 사용자 정보를 꺼낸다. return sqlSession.selectOne( "org.zerock.mapper.UserMapper.checkUserWithSessionKey" ,sessionId); } } |
5) UserService 인터페이스와 UserServiceImpl 클래스 수정하기
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | package org.zerock.service; import java.sql.Date; import org.zerock.domain.UserVO; public interface UserService { public UserVO login(UserVO dto); // 자동로그인 체크한 경우에 사용자 테이블에 세션과 유효시간을 저장하기 위한 메서드 public void keepLogin(String uid, String sessionId, Date next); // 이전에 로그인한 적이 있는지, 즉 유효시간이 넘지 않은 세션을 가지고 있는지 체크한다. public UserVO checkUserWithSessionKey(String sessionId); } |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 | package org.zerock.service; import java.sql.Date; import javax.inject.Inject; import org.springframework.stereotype.Service; import org.zerock.domain.UserVO; import org.zerock.persistence.UserDAO; @Service public class UserServiceImpl implements UserService { @Inject UserDAO dao; @Override public UserVO login(UserVO dto) { return dao.login(dto); } @Override public void keepLogin(String uid, String sessionId, Date next) { dao.keepLogin(uid, sessionId, next); } @Override public UserVO checkUserWithSessionKey(String sessionId) { return dao.checkUserWithSessionKey(sessionId); } } |
6) UserController에서 로그인 성공하고 쿠키사용 체크한 경우에 사용자 테이블에 세션id와 유효시간 처리해주기
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 | package org.zerock.controller; import java.sql.Date; import javax.inject.Inject; import javax.servlet.http.Cookie; import javax.servlet.http.HttpServletResponse; import javax.servlet.http.HttpSession; import org.springframework.stereotype.Controller; import org.springframework.ui.Model; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMethod; import org.zerock.domain.UserVO; import org.zerock.service.UserService; @Controller public class UserController { @Inject // byType으로 자동 주입 UserService service; // 로그인 폼을 띄우는 부분 @RequestMapping (value= "/login" ,method=RequestMethod.GET) public String loginForm(){ return "login/loginForm" ; // /login/loginForm.jsp를 띄움. } // end of loginForm // 로그인 처리하는 부분 @RequestMapping (value= "/loginProcess" ,method=RequestMethod.POST) public String loginProcess(HttpSession session,UserVO dto, HttpServletResponse response){ String returnURL = "" ; if ( session.getAttribute( "login" ) != null ){ // 기존에 login이란 세션 값이 존재한다면 session.removeAttribute( "login" ); // 기존값을 제거해 준다. } // 로그인이 성공하면 UserVO 객체를 반환함. UserVO vo = service.login(dto); if ( vo != null ){ // 로그인 성공 session.setAttribute( "login" , vo); // 세션에 login인이란 이름으로 UserVO 객체를 저장해 놈. returnURL = "redirect:/board/listPage" ; // 로그인 성공시 게시글 목록페이지로 바로 이동하도록 하고 /* * [ 세션 추가되는 부분 ] */ // 1. 로그인이 성공하면, 그 다음으로 로그인 폼에서 쿠키가 체크된 상태로 로그인 요청이 왔는지를 확인한다. if ( dto.isUseCookie() ){ // dto 클래스 안에 useCookie 항목에 폼에서 넘어온 쿠키사용 여부(true/false)가 들어있을 것임 // 쿠키 사용한다는게 체크되어 있으면... // 쿠키를 생성하고 현재 로그인되어 있을 때 생성되었던 세션의 id를 쿠키에 저장한다. Cookie cookie = new Cookie( "loginCookie" , session.getId()); // 쿠키를 찾을 경로를 컨텍스트 경로로 변경해 주고... cookie.setPath( "/" ); int amount = 60 * 60 * 24 * 7 ; cookie.setMaxAge(amount); // 단위는 (초)임으로 7일정도로 유효시간을 설정해 준다. // 쿠키를 적용해 준다. response.addCookie(cookie); // currentTimeMills()가 1/1000초 단위임으로 1000곱해서 더해야함 Date sessionLimit = new Date(System.currentTimeMillis() + ( 1000 *amount)); // 현재 세션 id와 유효시간을 사용자 테이블에 저장한다. service.keepLogin(vo.getUserId(), session.getId(), sessionLimit); } } else { // 로그인에 실패한 경우 returnURL = "redirect:/login" ; // 로그인 폼으로 다시 가도록 함 } return returnURL; // 위에서 설정한 returnURL 을 반환해서 이동시킴 } // 로그아웃 하는 부분 @RequestMapping (value= "/logout" ) public String logout(HttpSession session) { session.invalidate(); // 세션 전체를 날려버림 // session.removeAttribute("login"); // 하나씩 하려면 이렇게 해도 됨. return "redirect:/board/listPage" ; // 로그아웃 후 게시글 목록으로 이동하도록...함 } } // end of controller |
아까 쿠키를 생성해서 세션id 저장한 부분 바로 아래에다가 사용자 테이블에도 세션 id와 유효시간을 저장해 놓아야함!
이후, AuthenticationInterceptor의 preHandle() 부분에서
세션에 UserVO 객체가 null이 아닌 경우는 로그인 되어 있는 부분이니까 그대로 처리되도록 놔두고, 세션의 UserVO 객체가
null이지만, 쿠키가 null이 아닌 경우 쿠키에서 sessionId를 꺼내와서 사용자 객체를 반환받도록 작업할 것이다.
7) AuthenticationInterceptor에서 자동 로그인의 핵심 부분을 처리하자.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 | package org.zerock.interceptor; import javax.inject.Inject; import javax.servlet.http.Cookie; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import javax.servlet.http.HttpSession; import org.springframework.web.servlet.ModelAndView; import org.springframework.web.servlet.handler.HandlerInterceptorAdapter; import org.springframework.web.util.WebUtils; import org.zerock.domain.UserVO; import org.zerock.service.UserService; // 로그인처리를 담당하는 인터셉터 public class AuthenticationInterceptor extends HandlerInterceptorAdapter{ @Inject UserService service; // preHandle() : 컨트롤러보다 먼저 수행되는 메서드 @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { // session 객체를 가져옴 HttpSession session = request.getSession(); // login처리를 담당하는 사용자 정보를 담고 있는 객체를 가져옴 Object obj = session.getAttribute( "login" ); if ( obj == null ){ // 로그인된 세션이 없는 경우... // 우리가 만들어 논 쿠키를 꺼내온다. Cookie loginCookie = WebUtils.getCookie(request, "loginCookie" ); if ( loginCookie != null ){ // 쿠키가 존재하는 경우(이전에 로그인때 생성된 쿠키가 존재한다는 것) // loginCookie의 값을 꺼내오고 -> 즉, 저장해논 세션Id를 꺼내오고 String sessionId = loginCookie.getValue(); // 세션Id를 checkUserWithSessionKey에 전달해 이전에 로그인한적이 있는지 체크하는 메서드를 거쳐서 // 유효시간이 > now() 인 즉 아직 유효시간이 지나지 않으면서 해당 sessionId 정보를 가지고 있는 사용자 정보를 반환한다. UserVO userVO = service.checkUserWithSessionKey(sessionId); if ( userVO != null ){ // 그런 사용자가 있다면 // 세션을 생성시켜 준다. session.setAttribute( "login" , userVO); return true ; } } // 이제 아래는 로그인도 안되있고 쿠키도 존재하지 않는 경우니까 다시 로그인 폼으로 돌려보내면 된다. // 로그인이 안되어 있는 상태임으로 로그인 폼으로 다시 돌려보냄(redirect) response.sendRedirect( "/login" ); return false ; // 더이상 컨트롤러 요청으로 가지 않도록 false로 반환함 } // preHandle의 return은 컨트롤러 요청 uri로 가도 되냐 안되냐를 허가하는 의미임 // 따라서 true로하면 컨트롤러 uri로 가게 됨. return true ; } // 컨트롤러가 수행되고 화면이 보여지기 직전에 수행되는 메서드 @Override public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception { // TODO Auto-generated method stub super .postHandle(request, response, handler, modelAndView); } } |
AuthenticationInterceptor에서도 DB에 접근해서 처리를 해야함으로 UserService를 필드변수에 선언해주고 자동 주입을 위해
@Inject해주었다.
또한 preHandle() 메서드에서 로그인 세션이 없으면서 WebUtils를 이용해 쿠키를 가져온 뒤
로그인 세션이 없지만, loginCookie가 존재하는 경우 웹브라우저를 새로 켜고 로그인을 하지는 않았지만 이전에 로그인 하면서
쿠키 체크를 해논 유효기간이 남아 있는 경우임으로 service.checkUserWithSessionKey() 메서드를 통해 DB에서 유효기간이 남아있고
해당 세션id를 가지고 있는 사용자 정보를 받아온다.
그리고 마지막으로 해당 사용자 정보로 세션의 login을 세팅해주면 자동로그인에 필요한 모든 작업이 완료되었다.
8) 로그아웃 처리(UserController에서...)
로그아웃 처리를 깜빡하고 마지막이랬다.. 하하하....
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 | // 로그아웃 하는 부분 @RequestMapping (value= "/logout" ) public String logout(HttpSession session,HttpServletRequest request, HttpServletResponse response) { Object obj = session.getAttribute( "login" ); if ( obj != null ){ UserVO vo = (UserVO)obj; // null이 아닐 경우 제거 session.removeAttribute( "login" ); session.invalidate(); // 세션 전체를 날려버림 //쿠키를 가져와보고 Cookie loginCookie = WebUtils.getCookie(request, "loginCookie" ); if ( loginCookie != null ){ // null이 아니면 존재하면! loginCookie.setPath( "/" ); // 쿠키는 없앨 때 유효시간을 0으로 설정하는 것 !!! invalidate같은거 없음. loginCookie.setMaxAge( 0 ); // 쿠키 설정을 적용한다. response.addCookie(loginCookie); // 사용자 테이블에서도 유효기간을 현재시간으로 다시 세팅해줘야함. Date date = new Date(System.currentTimeMillis()); service.keepLogin(vo.getUserId(), session.getId(), date); } } return "redirect:/board/listPage" ; // 로그아웃 후 게시글 목록으로 이동하도록...함 } |
9) /views/board/listPge.jsp 에서 로그아웃 버튼 하나 넣기
<a href="/logout">[로그아웃]</a> 하나 추가하자 로그아웃 버튼!
10) 결과 확인하기
1) 로그인 하면서 로그인 상태를 기억하시겠습니까를 클릭하고 로그인 하는 모습
2) 로그인 한 직후 모습
3) 로그인 했음으로 글 작성 바로 잘 된다.
4) 로그아웃 버튼을 눌렀다. -> 눌렀지만, 내가 로그아웃 처리후 redirect를 listPage로 했기 때문에 다시 현재 화면이 뜸
5) 하지만 분명한건 로그아웃하고 다시 글등록 누르면 로그인 폼으로 이동된다는거~
여기까지해서 캐시 + 세션 + 인터셉터를 응용한 자동 로그인 구현을 마치겠습니다.