[Spring MVC] 쿠키+세션을 사용한 로그인

이전 글 : https://eckrin.tistory.com/entry/Spring-MVC-%EC%BF%A0%ED%82%A4-%EC%84%B8%EC%85%98

 

쿠키를 사용해서 로그인을 할 수 있지만, 쿠키 값은 클라이언트가 임의로 변경 가능하다. 또한 쿠키에 저장된 정보도 가져가서 보관할 수 있기 때문에, 보안상의 문제가 있다. (클라이언트쪽에 보관되는 것들은 무조건 보안에 위험이 있다.)

 

따라서 사용자별로 예측 불가한 임의의 토큰을 노출해서 클라이언트 쪽에서 해킹이 이루어져도 찾을 수 없게 만들고, 서버에서 토큰과 사용자 id를 매핑해서 인식한 후, 그 토큰을 서버에서 관리하게 하면 된다. 또한 토큰 자체의 만료시간을 설정해서 토큰을 악용하지 못하게 하자.

 

 

세션, 쿠키 활용

 세션 기반 인증의 stateful 서버는 클라이언트로 요청이 있을 때마다 클라이언트의 상태를 유지한다.

 

 클라이언트가 id와 pw등의 정보를 서버에 전달하면, 서버는 전달된 정보를 확인한 후 세션을 생성한다. 그 후 서버는 클라이언트에 세션 id를 쿠키에 담아서 전달하고, 클라이언트는 쿠키 저장소에 받은 세션 id를 보관한다. 여기서 포인트는 회원과 관련된 정보는 전혀 클라이언트에 전달하지 않으며, 추정 불가능한 세션 id만 쿠키를 통해서 클라이언트에 전달해서 사용자 추정이 불가능하게 만든다.

 이후 서버로 쿠키가 넘어오면, 서버에서는 쿠키에 담긴 세션id를 key로 해서 세션 저장소[key:세션id, value:클라이언트 정보(memberId등)]만 조회해서 정보를 반환하면 된다. 그러면 세션 id를 변환해도 의미가 없고, 클라이언트와 서버 사이에 오가는 쿠키에도 중요한 정보(전 포스트에서는 memberId)가 없으니 탈취해도 의미가 없게 된다.

 

 

세션 직접 구현하기

더보기

 직접 구현을 하기 위해서는 크게 세션 생성, 세션 조회, 세션 만료(제거)의 3가지 처리를 해주면 된다. 

public static final String SESSION_COOKIE_NAME = "mySessionID";
private Map<String, Object> sessionStore = new ConcurrentHashMap<>(); //해시맵에 동시성요청 고려

 

생성

//세션 생성
public void createSession(Object value, HttpServletResponse response) {
    //세션 id 생성
    String sessionId = UUID.randomUUID().toString();
    sessionStore.put(sessionId, value);

    //쿠키 생성
    Cookie mySessionCookie = new Cookie(SESSION_COOKIE_NAME, sessionId); //name, value
    response.addCookie(mySessionCookie);
}

가장 먼저 세션을 다룰 클래스를 @Component 어노테이션을 통해서 구성요소로 지정하고, 생성, 조회, 제거의 메소드를 구현한다. 생성의 경우 HashMap등으로 세션 저장소를 만든 뒤, 랜덤하게 생성된 세션id를 key로, 받은 클라이언트 정보를 value로 하도록 세션 저장소에 저장한 후에 쿠키를 생성하여 클라이언트에 전달하면 된다.

 

 

조회

//세션 조회
public Object getSession(HttpServletRequest request) {
    Cookie sessionCookie = findCookie(request, SESSION_COOKIE_NAME);
    return sessionStore.get(sessionCookie.getValue());
}

public Cookie findCookie(HttpServletRequest request, String cookieName) {
    if(request.getCookies()==null)
        return null;

    return Arrays.stream(request.getCookies())
            .filter(cookie -> cookie.getName().equals(cookieName))
            .findFirst()
            .orElse(null);
}

클라이언트의 요청에서 쿠키를 찾아서, 쿠키의 value값(세션 id)으로 세션 저장소를 조회한 뒤 리턴한다.

 

 

만료

//세션 만료
public void expire(HttpServletRequest request) {
    Cookie sessionCookie = findCookie(request, SESSION_COOKIE_NAME);
    if(sessionCookie!=null)
        sessionStore.remove(sessionCookie.getValue());
}

클라이언트의 요청에서 쿠키를 찾아서 쿠키의 valu값(세션 id)로 세션 저장소를 조회한 뒤 제거한다. 

 

 

 

서블릿 HTTP 세션 1 (HttpSession)

 서블릿에서 제공하는 HttpSession이라는 기능도 위에서 직접 구현해본 세션과 유사하게 동작하고, 일정시간 사용하지 않으면 세션이 삭제되는 기능도 추가되어있다.

서블릿을 통해서 HttpSession을 생성하면, 이름이 JSESSIONID이고, 값은 랜덤값을 가지는 쿠키를 생성한다.

 

@PostMapping("/login")
public String login(@Valid @ModelAttribute LoginForm form, BindingResult bindingResult, HttpServletRequest request) {
    if(bindingResult.hasErrors()) {
        return "login/loginForm";
    }

    Member loginMember = loginService.login(form.getLoginId(), form.getPassword());

    if(loginMember==null) {
        bindingResult.reject("loginFail", "아이디 또는 비밀번호가 맞지 않습니다");
        return "login/loginForm";
    }

    //로그인 성공 처리
    
    //서블릿의 HttpSession을 이용하기
    //세션이 있으면 있는 세션, 없으면 신규 세션 반환
    HttpSession session = request.getSession();
    //세션에 로그인 정보 보관
    session.setAttribute(SessionConst.LOGIN_MEMBER, loginMember);


    return "redirect:/";
}

먼저 로그인 컨트롤러에서는, request에서 getSession()으로 값을 받아서 세션 객체를 생성하고, 키(상수)와 value(객체)로 세션에 로그인 정보를 보관한다.

@PostMapping("/logout")
public String logout(HttpServletRequest request) {
    HttpSession session = request.getSession(false);
    if(session!=null)
        session.invalidate();
    return "redirect:/";
}

로그아웃 컨트롤러에서는 역시 request에서 getSession()을 통해서 세션 객체를 생성하되, 인자로 false를 넣어서 세션을 가져오는데 실패하더라도 새로운 세션을 만들지 않고 null을 반환하도록 한 후에, 존재한다면 제거해주면 된다.

@GetMapping("/")
public String homeLogin(HttpServletRequest request, Model model) {

    HttpSession session = request.getSession(false);
    //세션에 값이 없으면 홈으로
    if(session==null)
        return "home";

    //세션이 유지되면 로그인으로 이동
    Member loginMember = (Member)session.getAttribute(SessionConst.LOGIN_MEMBER);
    if(loginMember==null)
        return "home";

    model.addAttribute("member", loginMember);
    return "loginHome";
}

마지막으로 홈페이지 접속 컨트롤러에서는 request에서 getSession으로 얻어온 후, 세션이 존재하지 않거나 세션내에 값이 없다면 로그인 페이지로, 아니면 로그인된 화면으로 이동시킨다.

 

 

 

서블릿 HTTP 세션 2 (SessionAttribute)

@GetMapping("/")
public String homeLoginSpring(@SessionAttribute(name = SessionConst.LOGIN_MEMBER, required = false)Member loginMember, Model model) {

    if(loginMember==null)
        return "home";

    model.addAttribute("member", loginMember);
    return "loginHome";
}

추가로 세션을 조회할때는, 스프링에서 제공하는 @SessionAttribute를 이용하면 세션 존재 여부를 확인할 필요 없이 사용할 수 있다. (이 기능은 세션을 생성/삭제하지 않고, 조회에만 이용된다)

 

 

세션 타임아웃 설정

세션의 타임아웃 시간은 JSESSIONID를 전달하는 마지막 HTTP요청에서부터 timeout시간을 더해서 계산한다.

server.servlet.session.timeout=600

application.properties에서 세션의 타임아웃 시간을 초 단위로 설정할 수 있다. (글로벌 설정의 경우 분 단위)