본문 바로가기

JAVA/Spring

Spring login with cookie

반응형

Spring login with cookie (자동 로그인과 쿠키)


최근의 웹페이지를 보면 로그인할 때 체크박스를 만들어두고, 사용자가 원하는 경우 매번 로그인을 하지 않는 '자동로그인' 기능을 제공는 경우가 많다.
흔히 'Remember Me'로 표현하는데, 국내 사이트에서는 '자동 로그인'으로 표시하는 경우가 많다.
자동로그인은 브라우저가 서버에 접속할때 특정한 쿠키를 같이 전송하고, 이를 이용해서 로그인을 처리하는 방식이다.
HttpSession에서 사용되는 세션쿠키와 달리 개발자가 만들어 내는 쿠키의 경우는 '만료기한'을 지정할 수 있기 때문에, 오랜 기간 보관이 가능하다.

일반적으로 웹에서 로그인은 세션을 사용하는 방식을 더 선호 했는데, 가장 큰 이유는 보안에 대한 문제이다.
세션을 이용하는 경우 로그인한 사용자의 정보는 서버 내부에서만 사용되기 때문에 보안상 유리하다.
반면 쿠키는 매번 브라우저와 서버 사이에서 주고받는 방식으로 동작하기 때문에, 쿠키에 특정한 값이 기록된 경우 보안에 취약하다는 단점이 있다
또한 세션과 달리 쿠키에는 길이의 제한, 문자열만 보관할 수 있다는 문제 등이 존재 했기 때문에 과거에는 꺼려지는 방식으로 여겼다.
쿠키가 자동 로그인으로 적극적으로 활용된 계기는 모바일에서 매번 로그인하기가 힘들다는 문제에 대한 대안으로 사용되면서부터이다.
모바일 장비에서 매번 키보드로 아이디와 패스워드를 작성하는 일이 번거롭기 때문에, 로그이한 정보를 오랜 시간 유지할 수 있는 쿠키가 다시 유행하고 있다.


쿠키를 이용하는 자동 로그인 방식
자동 로그인을 처리하기 전에 우선 사용자가 로그인한 후 쿠키를 만들어서 브라우저로 전송하고, 다시 서버에 접속할 때 쿠키가 전달되는지 확인한다.

LoginInterceptor에서의 쿠키 생성
LoginInterceptor의 경우 postHandler()를 이용해서 HttpSession에 UserVO 타입의 객체를 보관한다.
이를 수정해서 중간에 쿠키를 생성하고 HttpServletResponse에 같이 담아서 전송하도록 코드를 수정한다.

public void postHandler(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {

HttpSession session = request.getSession();


ModelMap modelMap = modelAndView.getModelMap();

Object userVO = modelMap.get("userVO");


if(userVO != null){

session.setAttribute(LOGIN, userVO);


if(request.getParameter("useCookie") != null) {

Cookie loginCookie = new Cookie("loginCookie", session.getId());

loginCookie.setPath("/");

loginCookie.setMaxAge(60 * 60 * 24 * 7);

response.addCookie(loginCookie);

}


Object dest = session.getAttribute("dest");


response.sendRedirect(dest != null ? (String)dest : "/");

}

}

자동로그인을 선택한 경우 쿠키를 생성하고 생성된 쿠키의 이름은 loginCookie로 지정한다.
생성된 loginCookie에는 값으로 현재 세션의 아이디 값을 보관한다.
세션 아이디는 세션 쿠키의 값을 의미한다.
세션 쿠키의 경우 브라우저가 종료하면 사라지지만, 작성하는 loginCookie의 경우 오랜 시간 보관되기 위해서 setMaxAge()를 이용ㅎ안다.
setMaxAge()는 초 단위의 시간 동안 유효하므로 60 * 60 * 24 * 7를 이용해서 일주간 브라우저에서 보관된다.
만들어진 쿠키는 HttpServletResponse에 담겨서 전송한다.
로그인한 후 에는 게시물의 여러 페이지에서 매번 브라우저에 'loginCookie'가 전달된다.

로그인후

Connection:keep-alive
Cookie:_ga=GA1.1.2113203176.14333211;JSESSIONID=6E46A35E49D82A7239CEA7A643CC1375;loginCookie=6E46A35E49D82A7239CEA7A643CC1375

'JSESSIONID'는 Tomcat에서 발행된 세션 쿠키의 이름
loginCookie는 인터셉터에 의해 만들어진 쿠키


브라우저 종료 후 다시 접속

Connection:keep-alive
Cookie:_ga=GA1.1.2113203176.14333211;loginCookie=6E46A35E49D82A7239CEA7A643CC1375

브라우저 종료한 후 다시 접속했을 때는 세션 쿠키가 없는 상황이므로 JSESSION와 같은 세션 쿠키는 존재하지 않지만, 일주일간 유효기간이 지정된 쿠키는 그 값을 유지한다.

세션 쿠키는 브라우저가 종료될 때 같이 종료되었기 때문에 매번 브라우저를 새로 실행하고 접속하면 새롭게 만들어진다.
반면에 loginCookie의 경우 로그인 할때 브라우저를 이용해서 보관된다.
loginCookie는 유효기간이 7일이므로 그 이상의 시간이 지난 후에 접속되면 loginCookie는 전송되지 않는다.

자동로그인 처리의 기본 아이디어는 '세션+쿠키'를 이용한다.
보통은 HttpSession만 이용하거나 쿠키만을 이용하지만, 두가지를 섞어서 사용한다.


쿠키와 세션으로 발생하는 상황
HttpSession에 login 이름으로 보관된 객체가 없고 loginCookie가 없는경우
- 로그인과 관련된 아무런 정보가 없으므로, 사용자는 로그인이 필요한 상황

HttpSession에 login 이름으로 보관된 객체가 있고 loginCookie가 없는경우
- 세션에 login이라는 이름으로 보관된경우는 현재 사용자가 로그인한 상황이 확실

HttpSession에 login 이름으로 보관된 객체가 없고 loginCookie가 존재하는 경우
- 사용자는 이전에 로그인을 한적이 있을수도 있다. 사용자는 이전에 로그인을 할때 loginCookie가 생성되었을 것이고, 어떤 이유로 인해 사용자는 브라우저를 종료했을 것
- 브라우저가 종료되면서, 세션 쿠키는 사라졌지만,  loginCookie는 7일 동안 보관되므로  loginCookie가 있다는 것은 7일 사이에 접속한 적이 있다는 의미

HttpSession에 login 이름으로 보관된 객체가 있고 loginCookie가 존재하는 경우
- 사용자는 현재 접속중인 사용자

자동로그인은  HttpSession에는 login 이름으로 보관된 객체가 없지만, loginCookie는 존재하는 상황
이 경우 사용자는 이전에 로그인을 한적이 있다는 것을 의미하므로, 과거 로그인 시점에 기록된 정보를 이용해서 다시 HttpSession에 login이름으로 userVO 객체를 보관해줘야 한다.
이후 모든 작업은 HttpSession에 login 이름으로 저장된 객체가 있으므로 모두 해결


자동로그인의 구현

loginCookie에 있는 값을 이용해서 데이터 베이스에서  UserVO의 정보를 읽어오고, 읽어온 UserVO 객체를 현재의 HttpSession에 보관하면 로그인이 된다

-사용자가 로그인하면 데이터베이스에 현재 세션 의 ID값과 유효기간을 기록한다
-사용자가 로그인하지 않는 상태에서 쿠키를 가지고 접속하면 쿠키의 내용을 추출한다.
-쿠키의 내용물로 데이터베이스를 조회해서 유효기간에 맞는 값인지 확인한다.
-확인된 사용자는 세션에 로그인한 정보를 기록해서, 자동으로 로그인이 되도록 한다.

데이터베이스
sessionkey 칼럼과 sessionlimit 칼럼 추가


UserController

  @RequestMapping(value="/loginPost", method=RequestMethod.POST)

public void loginiPOST(LoginDTO dto, HttpSession session, Model model) throws Exception {

UserVO vo = service.login(dto);


if(vo == null) {

return;

}


model.addAttribute("userVO",vo);


//

if(dto.isUseCookie()){

int amount = 60 *60 *24 *7;


Date sessionLimit = new Date(System.currentTimeMillis()+(1000*amount));

service.keepLogin(vo.getUid(), session.getId(), sessionLimit);

}

}

loginCookie 값이 유지되는 시간 정보를 데이터베이스에 저장



AuthInterceptor

  @Override

public boolean preHandler(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {


HttpSession session = request.getSession();l


if(session.getAttribute("login") == null) {

logger.info("current user is not logined");


saveDest(request);


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

if(loginCookie != null) {

UserVO userVO = service.checkLoginBefore(loginiCookie.getValue());


logger.info("USERVO" + userVO);


if(userVO != null){

session.setAttribute("login", userVO);

return true;

}

}


resoponse.sendRedirect("/user/login");

return false;

}

return true;

}

현재 사용자가 HttpSession에 적당한 값이 없는경우 loginCookie를 가지고 있는지 체크한다.
과거에 보관한 쿠키가 있다면 UserService 객체를 이용해서 사용자의 정보가 존재하는 지 확인한다.
사용자의 정보가 존재한다면 HttpSession에 다시 사용자의 정보를 넣어주게 된다.


자동 로그인 테스트

로그인하지 않는 사용자가 로그인이 필요한 URI에 접근하는 경우 : 로그인 페이지로 이동
로그인할때 'Remember Me'를 체크하지 않는 상태에서 로그인 실행, 이후 로그인이 필요한 페이지로 이동: 정상적인 이동
브라우저를 종료하고 테스틀 다시 진행 : HttpSession이 변경됐으므로, 로그인 페이지로 이동
로그인 화면에서 'Remember Me'를 선택하고 로그인
브라우저를 종료하고 다시 실행한 후 로그인이 필요한 URI 접속: 정상적인 이동


로그아웃 처리
로그아웃 처리는 HttpSession 인경우 login과 같이 저장된 정보를 삭제하고, invalidate()를 주는 작업과 쿠키의 유효시간을 변경하는 작업으로 이루어진다.
자동로그인에 데이터베이스를 이용했다면 데이터베이스의 갱신도 함께 이뤄져야한다.
UserController에서 로그아웃의 처리를 위해서 직접 파라미터로 HttpServletRequest 등을 받는 방식이 가장 단순하고 별도의 인터셉터를 이용하는 방식도 사용해볼만 하다.

@RequestMapping(value="/logout", method = RequestMapping.GET)

public String logout(HttpServletRequest request, HttpServletResponse reponse, HttpSession session) throws Exception {

Object object = session.getAttribute("login");


if(object != null) {

UserVO vo = (UserVO)object;


session.removeAttribute("login");

session.invalidate();


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


if(loginCookie != null) {

loginCookie.setPath('/');

loginCookie.setMaxAge(0);

response.addCookie(loginCookie);

sevice.keepLogin(vo.getUid(), session.getId(), new Date());


}

}

return "user/logout";

}

보완이 필요한 부분
자동로그인이 어떤식으로 구현이 가능한지를 설명했지만, 실제로는 반드시 암호화에 대한 처리가 이루져야만 한다.
웹에 세션을 사용하는 것 역시 수많은 사용자가 새로운 세션을 짧은 시간 내에 생성하면 서버에 많은 부하를 주게 된다.
흔히 '세션 폭탄'이라는 공격 방식은 모든 쿠키를 차단하고 사이트에 접속하여 매번 서버에서 새로운 세션 저장 공간과 세션 아이디를  발급하게 해서 서버에 부하를 주는 방법이다.

암호화에 대한 처리와 더불어 고민해야 봐야하는 것은 '스프링 스큐리티'를 적용하는 것이다.
스프링 스큐리티는 보안을 처리하기 위한 스프링의 하위 프레임워크이다.
다만 스프링 시큐리티를 적용하기 위해서는 여러 용어와 개념을 새로 익혀야하는 학습과정이 필요하다.
만일 업무에 따른 권한의 구분이 복잡하다면, 인터셉터보다 차라리 처음부터 스프링 시큐리티를 고려하는 것이 좋다고 생각한다.
보완하고 싶은 부분은 AuthInteceptor가 직접 UserService를 호출하는 방식이 매끄럽지 않다는 점

개인의 취향이긴 하지만 컨트롤러에서 가능하면 코드를 간결하게 하기 위해서 HttpServletRequest 등을 사용하지 않도록 설계했기 때문에 부득이하게 인터셉터에서 직접 서비스 객체를 주입받아서 사용하고 있다.

웹개발에 있어서 세션 트래킹(흔히 로그인 처리)는 필수적인 항목이지만, 개발 초기에 신경을 쓰다보면 개발 시간을 지연하는 계기가 되기도 한다.
기능 기능의 개발이 끝난후 인터셉터와 쿠키 등을 활용해서 로그인을 처리해보았다.

인터셉터를 이용해서 특정 컨트롤러의 동작 전과 후에 대한 처리를 할수 있다.
필터와 달리 인터셉터는 스프링의 영역에 속하므로, 스프링의 기능등을 활용할수 있다.
세션을 이용하는 경우 단점은 브라우저의 종료와 함께 사용자의 연결정보도 같이 잃어버리게 된다. 반면 쿠키는 보안상 에 약점을 가진다.
쿠키에 세션의 정보를 담는 형태를 활용해, 쿠키를 가지고 로그인하는 경우 세션을 활용하도록 수정하는 것을 구현한다.

반응형

'JAVA > Spring' 카테고리의 다른 글

Spring Boot  (0) 2017.12.19
MyBatis Mapper  (0) 2017.12.18
Spring Interceptor HttpSession Login  (0) 2017.12.13
Spring Interceptor  (0) 2017.12.13
Spring Transaction  (0) 2017.11.27