Interceptor 자동 로그인 - Interceptor jadong logeu-in

쿠키를 통해서 세션아이디와 일치하는, '자동로그인 했던 회원의 정보' 를 불러온 뒤

다시한번 세션에 저장해버리는 로직을 구현 하기위해,

불러온 회원 정보를 어딘가에 재활용 할수있도록 보관해야합니다. 

바로 DB에 보관해야합니다.

DB에 쿠키의 만료시간과 쿠키의 값을 함께 저장하겠습니다.

sql을추가해서 컬럼을 2개 추가합니다.

alter table mvc_user ADD session_id varchar(80) default 'none' not null;
alter table mvc_user add limit_time date;

자동로그인 하지 않은사람은 default값으로 none이 저장 됩니다.

다음으로는 VO에는 DB에추가한 컬럼을 필드에 추가한 후

public class UserVO {
    /*이전필드 생략*/
    private String sessionId;
    private Date limitTime;
    
    public String getSessionId() {
        return sessionId;
    }
    public void setSessionId(String sessionId) {
        this.sessionId = sessionId;
    }
    public Date getLimitTime() {
        return limitTime;
    }
    public void setLimitTime(Date limitTime) {
        this.limitTime = limitTime;
    }
    
    @Override
    public String toString() {
        return "UserVO [account=" + account + ", password=" + password + ", name=" + name 
        + ", regDate=" + regDate + ", sessionId=" + sessionId + ", limitTime=" + limitTime 
        + ", autoLogin=" + autoLogin + "]";
    }
}

 setter()/getter() 와 toString()을 새롭게추가 합니다.


Mapper인터페이스에 Update 회원수정 쿼리문을 작동시킬 메서드 keepLogin( ) 추가

public interface IUserMapper {
    void keepLogin(String sessionId, Date limitDate, String account);
}

이곳의 sessionId는 쿠키에 저장된 sessionId입니다.


mapper.xml에 쿼리문 추가 / CRUD중에 수정을해줘야 하므로 update처리해줘야합니다.

아이디를 기준으로 쿠키에 저장된 sessionId 와 세션유효시간을 저장해줍니다.

<update id="keepLogin">
    UPDATE mvc_user 
    SET session_id=#{sessionId},
        limit_time=#{limitDate}
    WHERE account=#{account}
</update>	

회원정보가 insert 되어있는 상태에서, 로그인 할때 자동로그인을 체크안하면 session_id 값은 none이 됩니다. 반대로 체크를 한다면 none을 자동로그인 쿠키값에 담겨있는 세션아이디로 변경됩니다.

이때, limitTime또한 null에서 시간으로 수정해줘야합니다.

컨트롤러의 로그인을 수행하는 로그인메서드에서 날짜객체를 저장하는 로직을 추가합니다.

long limitTime = 60*60*24*90; //90일의 시간을 저장

//초를 날짜객체로 변환
//3개월 후의 시간을 만듭니다 - limitTime을 초로바꾸고 현재시간을 더해서 저장합니다.
long expiredDate = System.currentTimeMillis()+(limitTime*1000); //현재시간 + 3개월 = 3개월 후의 날짜
//삼개월 후의 시간을 날짜로 바꿉니다.
Date limitDate = new Date(expiredDate);//롱타입의 밀리초로 넣습니다.
service.keepLogin(session.getId(), limitDate, inputId);//DB에 세션정보와 시간 전달하기

limitTime 같은경우엔 초단위로 계산했기 때문에 날짜로 바꿔서 넣어야합니다.


초를 날짜객체로 변환하겠습니다.

 limitTime을 밀리초로바꿉니다.

limitTime*1000

하지만 이것은 딱 3개월이라는 날짜일 뿐이며 

3개월 후에 사라지게 하기 위해서는 날짜객체자체를 지금으로부터 3개월 후의 기간으로 만들어야 하기때문에

System.currentTimeMillis()

현재시간을 밀리초단위로 구하는 메서드를 호출하여 3개월에 더해줍니다.

 limitTime을 밀리초로 바꾸고 현재시간을 더해서 롱타입 변수에 저장합니다.

long expiredDate = System.currentTimeMillis()+(limitTime*1000); //현재시간 + 3개월 = 3개월 후의 날짜

이것이 지금으로부터 3개월 후가 되는 초단위 시간입니다.

삼개월 후의 시간이 초단위 이므로 다시 날짜로 바꾼 후 날짜객체에 저장합니다.

Date limitDate = new Date(expiredDate);

DB에 세션정보와 시간 전달합니다.

service.keepLogin(session.getId(), limitDate, inputId);

매개변수로 들어가는 session.getId()는 로그인 요청이 들어오면서 이미 쿠키에 저장된 sessionId값입니다

지금까지 한 작업은 쿠키에 저장되는 sessionId를 DB에도 저장해 주기 위한 작업의 진행과정 입니다.

로그인 요청할때 동시에 작업이 이루어 지는것 이기 때문에 두 값이 같게되는 것입니다.

이제 service와 service클래스에 메서드를 추가한 후 컨트롤러와 Mapper인터페이스를 연결합니다.


service

public interface IUserService {
    void keepLogin(String sessionId, Date limitDate, String account);
}

service클래스 

@Service
public class UserService implements IUserService {
	
    @Autowired
    private IUserMapper mapper
    
    @Override
    public void keepLogin(String sessionId, Date limitDate, String account) {      
        mapper.keepLogin(sessionId, limitDate, account);
    }
}

매퍼 xml에서 session_id=#{}, limit_time=#{}, account=#{} 셋 모두 VO에 있어서 VO객체를 통으로 전해줘도 되겠지만 클라이언트가 sessionId를 넘겨주진 않았습니다.

직접 조회를하고 쿠키에 넣어줬던 작업 기억하시죠?

String sessionId, Date limitDate, String account

그렇기 때문에 service메서드에는 mapper에 들어갈 값들을 객체로 넣을수 없기때문에 컨트롤러에서 호출할때와 같이 각개로 넣어줍니다.

하지만! 시험해보기위해 자동로그인을 체크하고 로그인을 수행했을때

BindingException:이라는 오류가 발생할 것입니다.

[arg2, arg1, arg0, param3, param1, param2]즉, 전달되는 데이터의 순서를 정해달라는것입니다.

@Service
public class UserService implements IUserService {
	
    @Autowired
    private IUserMapper mapper
    
    @Override
    public void keepLogin(String sessionId, Date limitDate, String account) {      
        mapper.keepLogin(sessionId, limitDate, account);
    }
}

mapper에서는 다량의 데이터를 즉, 매개변수가 두개이상을 담아 넘긴다면 한군데에 묶여있어야 합니다

객체로 포장해야한다는 것과 같습니다.


하지만 굳이 포장하기 싫을때에는 Map으로 포장해서 보내면 됩니다.

Service클래스에서 Mapper에게 전달할 때 각개적으로 주지않고 한군대에 묶어줍니다.

@Service
public class UserService implements IUserService {
	
    @Autowired
    private IUserMapper mapper
    
    @Override
    public void keepLogin(String sessionId, Date limitDate, String account) {
        //Map으로 묶기
        Map<String,Object> datas = new HashMap<>();
        datas.put("sessionId", sessionId); //키이릅도 같게합니다.
        datas.put("limitDate", limitDate);
        datas.put("account", account);
        mapper.keepLogin(datas);
    }

HashMap을사용해서 Mapper xml파일의 쿼리문에 #{ }에 들어갈 이름을 map의 key이름과 맞춰 넣어주면 되겠습니다.

이제 다시 서버를 실행하고 자동로그인 체크박스에 체크후 로그인을 진행해 보면

DB에 sessionId값과 session유효기간에 대한 값이 성공적으로 들어간걸 확인할 수 있습니다.


이때 DB에 들어온 SessionID는 고유한 아이디로써 절대 중복데이터가 나오지 않게됩니다.


현재 로그아웃 처리를 만들지 않았기때문에 새로운 아이디로는 로그인하지 않겠습니다.

로그아웃을 할때는 sessionID를 지워버려서 다시 디폴트값인 None으로 만들어야만 합니다
그렇게 해서 중복을 방지해야 하기 때문입니다만, 아직 로그아웃에 sessionId를 지우는 로직을 만들지 않았기 때문에
로그아웃 후 다른 아이디로는 로그인 하지 않습니다.

그렇다면 이제 쿠키값과 동일한 값을 가진 회원이 1명 존재합니다
자동로그인에 대한 쿠키값을 가지고 있는 로컬PC에서 브라우저 종료 후 사이트에 재진입 했을 때는 이 회원의 정보를 불러오도록 하여 불러들여온 회원정보를 세션에 저장함으로써 로그인을 처리하도록 구현해 보겠습니다.

쿠키를통해 회원정보를 읽을수있는 작업을 로직을 작성 하겠습니다.

	UserVO getUserWithSessionId(String sessionId);

쿼리문을 mapper에 입력해줍니다.

<!-- 자동로그인을 했던 회원이 사이트 재 방문시 로컬에 저장된 쿠키값과 일치하는 회원의 정보를 조회하는 SQL -->
<select id="getUserWithSessionId" resultMap="userMap">
    SELECT * FROM mvc_user 
    vWHERE session_id=#{sessionId}
</select>

그렇다면 재방문시 로그인을 어떻게 유지시킬것인가

 다시 사이트에 진입하는데 진입하자마자 특정한 세션아이디를 읽어들입니다,
접속한 pc의 로컬에 저장된 쿠키로부터 저장되있는 세션아이디를 읽어들이는 것입니다.
이것은 현재의 세션아이디가 아닌, 로컬에 저장된 쿠키로 부터 읽어들인 세션아이디 이므로

이전 자동 로그인에 대한 세션ID가 저장된 쿠키의 기록입니다.

쿠키로부터 읽어들인 세션아이디를 가지고 DB에게 물어봅니다
내가 읽어들인 자동로그인 세션아이디 값을 기준으로 회원의 정보를 가져와라.
회원정보를 받게되면 그 회원정보를 세션에 다시 setAttribute해줍니다.

로그인이라는이름으로.
이 작업으로 로그인이 처리가 됩니다.

 우리는 이제 이 작업을 인터셉터를 활용하여 작업 할것입니다.
사이트에 재 진입했을 때 쿠키값 읽어오고 그것에맞는 회원정보 조회해 와서 다시 세션에 담아버리는 자동 로그인 처리를

인터셉터에서 처리합니다.


AutoLogin이라는 인터셉터클래스를 만듭니다..

public class AutoLoginInterceptor extends HandlerInterceptorAdapter{
	
    @Inject
    private IUserService service;
	
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler)
            throws Exception {

        //1. 자동로그인 쿠키가 있는지 여부 확인 - 자동로그인 체크를 안했다면 로컬에 쿠키가 없습니다.

        // 원하는쿠키만 찾아오는 스프링방법
        Cookie loginCookie = WebUtils.getCookie(request, "loginCookie"); 
        //request객체로부터 찾고싶은쿠키의 이름넣어 해당쿠키가 존재한다면 쿠키객체를 찾아오고 없으면 null을 반환합니다.
        if(loginCookie != null) {//자동로그인 했다면
            //자동로그인 했기 때문에 회원정보를 가져와야합니다. 누가 자동로그인을 시도했는가...하고

            String sessionId = loginCookie.getValue(); //쿠키에서 세션아이디 가져오기
            //2. DB에서 쿠키값과 일치하는 세션아이디를 가진 회원의 정보를 조회
            UserVO user = service.getUserWithSessionId(sessionId);
            if(user!=null) {
                HttpSession session = request.getSession();
                session.setAttribute("login", user);
            }
        }
        return true;//자동로그인을 시키면서 통과할것인가 안시키면서 통과할것인가 무조건 통과는 시킵니다.
    }	
}


preHandle메서드를 오버라이딩하여 컨트로럴로넘어가기전에 걸러줍니다.

 Cookie loginCookie = WebUtils.getCookie(request, "loginCookie"); 
        if(loginCookie != null) {

            String sessionId = loginCookie.getValue(); 
            UserVO user = service.getUserWithSessionId(sessionId);
            if(user!=null) {
                HttpSession session = request.getSession();
                session.setAttribute("login", user);
            }
        }

인터셉터로 작업될 로직이 완료되었다면 해당 인터셉터를 빈등록 하고 인터셉터 설정을 합니다.

<beans:bean id="autoLoginInterceptor" class="com.spring.mvc.board.commons.interceptor.AutoLoginInterceptor" />

<interceptors>
    <interceptor>
        <mapping path="/"/>
        <beans:ref bean="autoLoginInterceptor"/>
    </interceptor>
<interceptors>

 서버를 실행하고 메인페이지에 접속이 됬을 때 새로운 아이디로 접속해보는 실습을 진행합니다.

기존 저장되있는 쿠키를 삭제하고 새로운 아이디로 자동로그인 요청을 보내면서 로그인을 시도해야 합니다.

F12개발툴을 누른 후 Ctrl+Shift+DELETE키를 눌러주면 쿠키를 삭제할 수 있습니다.

쿠키를 지우는 이유는 로그아웃을 눌러도 다시 홈으로 돌아갔을 때 컨트롤러 접근 이전에 쿠키를 확인하고 자동로그인을 시켜주는 인터셉터에 또 걸려버리므로, 로그아웃이 진행되지 않습니다.

쿠키를 지운다면 인터셉터에서 쿠키가 비어있지않을 때 로그인처리가 되는 if조건문 로직으로 넘어가지 않게되겠죠?

쿠키가 지워졌을때 새로운 아이디를 자동로그인으로 로그인 해주게되면 인터셉터에서 로컬로부터 쿠키값을 읽은 후 쿠키가 비여있으므로 컨트롤러로 넘어간 뒤 새로운 세션ID값이 쿠키에 등록이됨과 동시에 로그인을 시도한 새로운 아이디에 세션아이디와 세션의유효시간을 DB에 한번 더 저장해줍니다.

이렇게 쿠키를 지워주는 작업을 로그아웃 메서드에서 처리 해줘야합니다.

로그아웃 로직 순서로는
1. 쿠키를 지워버립니다.
2. DB의 세션을 None으로 바꿔야합니다.
이유는 로그아웃처리를할때 다시 홈으로 돌아가게되면 컨트롤러접근 이전에 자동로그인 인터셉터에 또 걸리게됩니다.

            Cookie loginCookie = WebUtils.getCookie(request, "loginCookie"); 
            if(loginCookie != null) {
                loginCookie.setMaxAge(0);
                response.addCookie(loginCookie);
                service.keepLogin("none", new Date(), user.getAccount());

로그아웃 시 자동로그인 쿠키 삭제 및 해당 회원 정보에서 session_id 제거

1. loginCookie를 읽어온 뒤 해당 쿠키가 존재하는지 여부 확인

Cookie loginCookie = WebUtils.getCookie(request, "loginCookie"); 


2. 쿠키가 존재한다면 쿠키를 삭제하는데 

Cookie loginCookie = WebUtils.getCookie(request, "loginCookie"); 
if(loginCookie != null) {
    loginCookie.setMaxAge(0);
               

쿠키의 수명을 0초로 다시 설정한 후 (setMaxAge)


3. 응답 객체 response를 통해 로컬에 0초짜리 쿠키 재전송 해 줍니다.

Cookie loginCookie = WebUtils.getCookie(request, "loginCookie"); 
if(loginCookie != null) {
    loginCookie.setMaxAge(0);
    response.addCookie(loginCookie);

 작업으로 쿠키를 삭제하게 되는것입니다. 유효기간이 0초이기때문에 저렇게 세팅하고 브라우저에서 로그아웃처리를 해 주는 순간 0초가 지나버리기 때문에 로컬에서 쿠키가 삭제됩니다.


4. service를 통해 keepLogin을 호출하여 DB컬럼 레코드 재설정해줍니다.

UserVO user = (UserVO)session.getAttribute("login");

service.keepLogin("none", new Date(), user.getAccount());

 Session에 저장되어있는 정보중 회원 아이디 user.getAccount();를 기준으로 세션의 시간을 현재 시간으로 고쳐줘야하고 쿠키를 "none"으로 저장하여 삭제합니다.

    @GetMapping("/logout")
    public ModelAndView logout(HttpSession session, HttpServletRequest request, HttpServletResponse response) {
        System.out.println("/user/logout 요청! ");
        UserVO user = (UserVO)session.getAttribute("login"); //getAttribute리턴타입은 Object이므로 UserVO객체로 다운캐스팅해야합니다.
        if(user != null) {
            session.removeAttribute("login"); //login에대한 세션객체의 모든 정보를 삭제합니다.
            session.invalidate();//session객체를 무효화합니다.
			
            //로그아웃 시 자동로그인 쿠키 삭제 및 해당 회원 정보에서 session_id 제거
            //1. loginCookie를 읽어온 뒤 해당 쿠키가 존재하는지 여부 확인
            //2. 쿠키가 존재한다면 쿠키를 삭제하는데 쿠키의 수명을 0초로 다시 설정한 후 (setMaxAge)
            //3. 응답 객체 response를 통해 로컬에 0초짜리 쿠키 재전송 -> 쿠키삭제
            //4. service를 통해 keepLogin을 호출하여 DB컬럼 레코드 재설정
            //(session_id -> "none" limit_time -> "현재시간으로")
            Cookie loginCookie = WebUtils.getCookie(request, "loginCookie"); 
            if(loginCookie != null) {
                loginCookie.setMaxAge(0);
                response.addCookie(loginCookie);
                service.keepLogin("none", new Date(), user.getAccount());
            }

			
        }
		return new ModelAndView("redirect:/");
    }

따지고보면 제기준에서는 로그아웃에 의한 쿠키삭제 처리가 더 우선적으로 진행된 후 인터셉터 처리를 하는게 맞지 않나 싶습니다.

인터셉터를 먼저 설정해버린다면 흐름파악이 조금 꼬이게될지도 모릅니다.

중간에 쿠키를 삭제할때 로그아웃 했을때 쿠키가 삭제되는 행위를 컨트롤러단에서 하지 않고 임의적으로 쿠키를 삭제를 해 버리기 때문에 개념 흐름이 조금 모호해질수 있습니다.