[Spring Framework] 로그인, 쿠키와 세션
로그인 기능을 구현하기 위해서는 서버가 로그인 한 사용자의 상태를 기억하고 있어야 한다. 어떤 사용자인지 식별하기 위해서는 쿠키와 세션이라는 두 가지를 이용할 수 있다.
쿠키는 서버가 클라이언트에게 쿠키 값을 넘기고, 브라우저가 이를 기억하고 있다가 다음에 서버에 요청할 때 HTTP 요청메세지에 쿠키를 함께 담아서 요청하는 방법이고, 세션은 세션 아이디를 서버가 기억하고 있다가 클라이언트의 요청으로 세션 아이디가 넘어오면 정보를 조회해서 응답하는 방법이다. 세션도 정보식별을 위해 쿠키를 사용한다.
쿠키 사용하기
쿠키는 위에서 설명했듯이 서버가 HTTP 응답에 쿠키를 담아서 브라우저에 전달하면 브라우저가 앞으로 요청을 보낼 때마다 요청 메시지에 쿠키를 담아 요청하는 방법이다.
쿠키에는 두 종류가 있다.
- 영속 쿠키: 만료 날짜를 입력하면 해당 날짜까지 유지되는 쿠키
- 세션 쿠키: 만료 날짜를 생략하면 브라우저 종료시까지만 유지되는 쿠키
보통 브라우저 종료시에 로그아웃되기를 기대하기 때문에 세션쿠리르 사용해 보도록 하겠다.
요청 메세지에서 쿠키 가져오기
사용자가 로그인에 성공하게 되면 Cookie 인스턴스를 만들고, 이를 HttpServletResponse 파라미터에 담아서 HTTP 응답 메시지에 쿠키를 추가할 수 있다.
// response: HttpServletResponse 타입 메서드 파라미터
CooKie idCookie = new Cookie("memberId", String.valueOf(loginMember.getId());
response.addCookie(idCookie);
Cookie 생성자에 전달한 내용은 빼고 response 에 쿠키 데이터를 전달한 부분에 집중하면 된다. rseponse 변수는 컨트롤러 메서드의 HttpServletResponse 타입 파라미터이다.
이렇게 한번 쿠키를 추가해서 응답메세지를 보내게 되면, 웹 브라우저가 종료되기 전까지 “memberId”라는 이름의 쿠키를 요청 메시지에 보내준다.
컨트롤러 메서드에서 쿠키를 받을 때 @CookieValue 애노테이션을 사용하여 편리하게 조회할 수도 있다. 요청메세지에 지정한 이름의 쿠키가 없다면 파라미터에 null을 넣어준다. 아래의 코드는 memberId라는 이름의 쿠키를 파라미터로 받아오는 메서드이다.
@GetMapping("/")
public String homeLogin(
@CookieValue(name = "memberId", required = false) Long memberId,
Model model
) {
...
}
required 옵션은 이 쿠키값이 필수인지 아닌지를 나타낸다. 쿠키 값이 없어도 접근할 수 있는 페이지인 경우에는 false로 두어야한다.
쿠키의 보안문제
쿠키를 사용해서 로그인과 관련된 ID를 전달하면 로그인을 유지할 수 있다. 그러나 쿠키에는 보안문제가 존재한다.
- 쿠키 값을 임의로 변경할 수 있다.
- 웹 브라우저의 개발자 모드에서 Application > Cookie로 들어가면 해당 URL을 요청할 때 전달될 쿠키 값을 볼 수 있고, 이 값을 직접 조작할 수 있다.
- 사용자를 구분하는 쿠키 값이 단순한 경우 값을 조작한 후 웹 페이지를 새로고침 하면 다른 사용자로 로그인한 상태가 된다.
- 쿠키에 보관된 값은 훔쳐갈 수 있다.
- 쿠키에 개인정보나 신용카드정보 같은 민감한 정보를 저장해 두었다면 해커에 의해서 정보가 유출될 수 있다.
- 쿠키정보는 웹 브라우저에도 보관되고 네트워크 요청마다 계속 클라이언트에서 서버로 전달되기 때문에 나의 로컬 PC나 네트워크 전송 구간에서 털릴 수 있다.
따라서 쿠키에 중요한 값을 노출하지 않고, 예측 불가능한 토큰만을 노출한 다음, 서버에서 토큰을 매핑해서 사용자를 인식하는 방법을 사용해야 한다.
- 토큰을 중간에 해커가 가로챈다고 해도 예상할 수 없는 정보로 담고
- 가로채도 지속적으로 사용할 수 없게 토큰의 만료시간을 짧게 유지해야 한다.
- 해킹이 의심되는 경우 토큰을 강제로 제거한다.
세션 사용하기
세션이란 것은 복잡한 것이 아니라 쿠키를 사용하면서 서버에서 데이터를 유지하는 방법일 뿐이다.
앞에서 말한 보안 문제를 해결하기 위해서 세션을 사용하고, 직접 자바 코드로 세션을 구현할 수도 있지만 서블릿이 공식 지원해 주는 세션을 주로 사용한다. 세션을 일정시간 동안 사용하지 않으면 세션을 삭제하는 기능도 제공해 준다.
HttpSession
HttpSession을 통해서 세션을 생성하면 JSESSIONID라는 이름의 쿠키에 추정불가능한 값을 생성해 넣어 세션 ID로 사용하게 된다.
HttpSession session = request.getSession();
session.setAttribute(SessionConst.LOGIN_MEMBER, loginMember);
getSession에는 boolean타입의 create 파라미터를 넘겨줄 수 있는데 이 값에 따라서 getSession의 동작이 달라집니다.
- request.getSession(true)
- 세션이 있으면 기존의 세션을 반환한다.
- 세션이 없으면 새로운 세션을 생성해서 반환한다.
- request.getSession(false)
- 세션이 있으면 기존 세션을 반환한다.
- 세션이 없으면 새로운 세션을 반환하지 않고 null을 반환한다.
세션이 있을 경우에 기존세션을 반환하는 동작은 동일하지만 기존에 세션이 없을 경우에 새로운 세션을 만들어서 반환할 것인지, 아니면 새로운 세션을 만들지 않을 것인지에 대한 차이가 있습니다.
세션에 데이터를 보관하는 방법은 setAttribute() 메서드를 사용하면 됩니다. 하나의 세션에 여러 값을 저장할 수 있습니다.
개발자 도구에 가서 확인해 보면 로그인을 했을 때 JSESSIONID 쿠키가 생성되는 것을 확인할 수 있습니다.
@SessionAttribute
스프링은 세션을 더 편리하게 사용할 수 있도록 @SessionAttribute애노테이션을 지원해 줍니다. @SessionAttribute 애노테이션과 세션의 이름을 지정하면 JSESSIONID에 맞는 객체를 자동으로 맵핑해 줍니다.
@GetMapping("/")
public String homeLoginV3Spring(
@SessionAttribute(name = SessionConst.LOGIN_MEMBER, required = false) Member loginMember,
Model model
) {
...
}
세션정보 확인하기, 타임아웃 설정
HttpServletRequest로 얻어진 HttpSession 인스턴스에서 어떤 정보를 얻어낼 수 있고, 타임아웃 설정은 어떤 방식으로 동작하는지 알아보자.
주요 필드들을 살펴보면 다음과 같다.
- sessionId : 세션 Id, JSESSIONID의 값이다.
- maxInactiveInterval : 세션의 유효 시간, 예) 1800초, (30분)
- creationTime : 세션 생성일시
- lastAccessedTime : 세션과 연결된 사용자가 최근에 서버에 접근한 시간, 클라이언트에서 서버로 sessionId (JSESSIONID)를 요청한 경우에 갱신된다.
- isNew : 새로 생성된 세션인지, 아니면 이미 과거에 만들어졌고, 클라이언트에서 서버로 sessionId(JSESSIONID)를 요청해서 조회된 세션인지 여부
세션데이터는 사용자가 로그아웃 요청을 했을 때 session.invalidate() 메서드가 호출되는 경우에 삭제됩니다. 하지만 사용자가 실제로 이용할 때는 보통 로그아웃보다는 브라우저를 그냥 종료하는 방식을 택하게 됩니다. HTTP 프로토콜은 비연결성(Connectionless) 한 성질을 가지기 때문에 사용자가 웹 브라우저를 종료했는지를 서버에서는 인식하지 못합니다.
이런 상황에서 JSESSIONID를 해커가 탈취하고 해당 쿠키로 악의적인 요청을 할 수 있습니다. 따라서 브라우저가 종료되더라도 세션이 언젠가는 종료되도록 하는 기법이 필요합니다.
기본적으로 세션은 메모리에 생성되기 때문에 꼭 필요한 경우에만 생성하는 것이 좋습니다. 세션에 저장하는 데이터도 전부 저장해 두는 것이 아니라 식별자만 저장해 둔 다음 나머지는 자바 코드로 데이터를 불러오는 방식을 사용합니다. 메모리의 크기가 무한하지 않기 때문에 곡 필요한 경우에 생성하고, 필요한 정보만을 저장하는 것이 좋습니다.
따라서 사용자가 서버에 최근 요청한 시간을 기준으로 30분 정도를 계속 유지해 줄 수 있습니다. 기본적으로 HttpSession이 이 방식을 사용하고 있습니다.
세션 타임아웃을 설정하기 위해서는 application.properties에 다음 옵션을 줘서 마지막 요청으로부터 얼마나 기다릴지 초단위로 설정할 수 있습니다.
sever.sevlet.session.timeout=60 // 최솟값이 60초,
// 글로벌한 설정은 분 단위로 설정해야한다. 60, 120, 180, ...
session.getLastAccessTime()으로 최근 세션 접근시간을 알 수 있고, 여기서 얻어진 마지막 요청시간 이후로 timeout만큼의 시간이 지나면 WAS가 내부에서 해당 세션을 제거합니다.
timeout 시간이 너무 길어지면 메모리 사용이 계속 누적될 수 있고, 그렇다고 너무 짧아지면 사용자 경험에서 불편함을 느낄 수 있기 때문에 적절한 시간을 따져서 선택해야 합니다.