저의 롤 전적 검색 사이트의 서비스 운영 방식은 회원가입을 한 사용자를 대상으로만 전적 검색을 지원하고 있습니다. 그 이유는 서비스의 본질이 전적 검색이 아닌, 댓글을 통하여 유저들의 전략 토론의 장을 만들기 위함이었고, 그를 위해선 댓글의 작성자를 구분할 수 있는 요인이 필요했기 때문입니다.
기본적으로 HTTP 프로토콜은 요청/응답 시 이전의 상태를 저장하지 않는 Stateless
가 원칙이기 때문에, 로그인한 사용자들은 매번 요청에 "야 서버야, 난 로그인한 사용자야" 라는 것을 알려줘야 합니다. 이를 위해서는 서버 측에서 유저의 로그인 정보를 파악하고 있어야 하겠죠? 그 때 세션을 사용하여 서버에서 클라이언트 인증 로직을 구현할 수 있습니다.
세션을 이용하여 인증하는 방식 이외에도
JWT
토큰을 이용하여 인증하는 방법 또한 존재하는데, 제가 프로젝트에 적용한 방식은 세션 인증 방식입니다.Spring Security
에 대한 이해가 좀 더 쌓이면JWT
토큰을 이용하는 방식으로 변경할 예정입니다.
세션은 Key - Value
형식으로 이루어져 있습니다. Key
에는 sessionId, value
에는 사용자 정보가 들어갑니다.
세션을 이용하여 인증하는 방식에 대하여 간단하게 설명드리겠습니다.
- 서버는 세션을 저장하는 세션 저장소를 두고 있습니다.
- 클라이언트가 로그인에 성공하면 서버는 세션 저장소에 새로운 클라이언트를 위한 맞춤 세션을 생성합니다.
- 서버는 로그인에 대한 응답으로 HTTP 응답에 session ID를 담아, 클라이언트에게 전송합니다.
- 클라이언트는 이제부터 매 요청에 서버로부터 전송 받은 session ID를 담아서 서버에 보냅니다.
- 서버는 자신의 세션 저장소에서 클라이언트가 보낸 session ID를 조회하여, 일치하는 session ID가 존재한다면 인증을 완료합니다.
이렇게만 설명하면, 쿠키
에 사용자 정보를 담아 통신하는 방식과 다른 점이 없는 것처럼 느껴질 수 있습니다. 하지만 큰 차이점 세션은 쿠키에 session ID
를 담아 인증 정보를 주고 받는 것입니다.
쿠키에 사용자 정보를 담아서 통신하면 어떤 문제가 발생할까요?
Cookie: 사용자ID=100
이런 식으로 저장이 되겠죠? 그런데 사용자 ID가 100이라는 것은, 99번 사용자, 101번 사용자가 있다는 것도 쉽게 예측이 가능합니다. 만약 클라이언트가 웹 브라우저에서 쿠키값을 101로 변경하면 , 101번 사용자의 명의로 서비스 이용이 가능해집니다. F12
- Application
- Storages
-Cookies
로 들어가면 쿠키 값을 변경할 수 있습니다. 이렇게 쿠키로 인증하는 방식은 보안상 적절하지 않습니다.
세션을 이용하면, 이러한 단점들을 극복할 수 있습니다. 어떻게 극복할 수 있을까요?
기본적으로 쿠키에 담아서 전달되는 session ID
는, 예측이 불가능한 문자열입니다. 클라이언트가 쿠키를 변조하려고 해도, 일정한 규칙성이 없으니 남의 session ID
를 예측할 수 없습니다. 또한, 해커가 중간에 탈취해봤자 아무런 정보를 얻을 수 없습니다. 오로지 서버의 세션 저장소에서 사용자 정보를 인증하기 위한 열쇠일 뿐입니다. 만약 해커가 session ID
를 알아내기 위해 브루트포스와 같은 무차별 대입 공격을 시도하면 어떨까요? 이를 예방하기 위해, 세션에는 만료 시간을 설정할 수 있습니다. 만료 시간이 끝나면 서버는 세션 저장소에서 세션을 삭제합니다. 그렇다면 해커가 무한히 대입 공격을 시도해도 일정 시간이 지나면 삭제되기 때문에 어느정도 대응이 가능합니다.
제 프로젝트 코드를 참고하겠습니다.
public abstract class SessionConst {
public static final String LOGIN_SESSION = "loginSession";
}
세션 키를 설정하기 위해 만든 추상 클래스입니다.
SessionConst
클래스는 세션을 사용하기 위해 만든 클래스일 뿐, 인스턴스를 만든다거나 하진 않기 때문에 추상 클래스로 선언하였습니다.
@PostMapping("/login_proc")
public String login(@ModelAttribute UserLoginDto userLoginDto, HttpServletRequest request){
// 로그인 성공 처리 (없으면 새로 세션을 생성해줌)
HttpSession session = request.getSession();
// 세션에 "loginSession"
session.setAttribute(SessionConst.LOGIN_SESSION, userLoginDto);
log.info("로그인 성공");
return "home";
}
클라이언트가 로그인하는 컨트롤러 메서드입니다. 필요한 부분만 추출했습니다.
HttpSession session = request.getSession();
- 요청으로 들어온 HttpRequest에서 세션을 꺼내는 로직입니다.
getSession()
의 인자값으로는 true, false를 전달할 수 있습니다. 저처럼 아무것도 전달하지 않으면 default 값으론 true가 설정되어 있습니다. 인자로 true를 넘기면, 요청에 기존 세션이 있으면 세션을 반환하고, 세션이 없으면 새로운 세션을 생성해서 반환합니다. 반대로 false를 넘기면, 세션이 없는 경우에는 새로운 세션을 생성하지 않고 null을 반환합니다. 클라이언트가 맨 처음 로그인할 경우에는 세션이 없을테니, 세션을 생성을 해서 반환해주기 위해 true로 전달했습니다.HttpSession
은 서블릿이 제공하고, HttpSession을 생성하면 다음과 같은 쿠키를 생성합니다. Cookie:JSESSIONID=5B78E23B513F50164D6FDD8C97B0AD05- 쿠키의 이름은 JSESSIONID이고, 값은 추정 불가능한 랜덤값입니다.
session.setAttribute(SessionConst.LOGIN_SESSION, userLoginDto);
- 생성한 세션에 Key - value 로 "loginSession" - userLoginDto 를 저장하여, 로그인한 회원 정보를 보관하는 것입니다. 서버는 사용자의 요청에서, 이름이 JSESSIONID인 쿠키를 찾아, 추정 불가능한 value를 얻을 것입니다. 그 value를 자신의 세션 저장소에 찾으며 인증 로직을 수행하게 됩니다.
@GetMapping("/")
public String homePage(
@SessionAttribute(name = SessionConst.LOGIN_SESSION, required = false) UserLoginDto userLoginDto, Model model) throws IOException {
// 세션에 회원 데이터가 없으면 일반 home 화면
if (userLoginDto == null) {
log.info("nullController");
return "home/home";
}
// 세션에 회원 데이터가 있으면 로그인 home 화면
return "home/loginHome";
}
세션으로 인증 로직이 수행됐는지의 여부에 따라, 서로 다른 홈페이지 화면을 보여주는 로직입니다.
@SessionAttribute
를 사용하면, 사용자의 요청에서 세션 쿠키를 꺼내 검증하는 로직을 한결 수월하게 구현할 수 있습니다.
server.servlet.session.timeout=1800
application.properties 에서 세션 만료 시간을 설정할 수 있습니다. 1800은 초를 의미하며, 기본 default값은 1800입니다. 30분이 지나면 세션이 만료되어 세션 저장소에서 삭제됩니다.
세션을 인증하는 방식에도 고려할 사항은 있습니다.
stateful
하기 때문에, 이 정보를 서버와 클라이언트가 모두 갖고 있어야 합니다. JWT
방식을 사용하면 해결이 가능하다고 합니다. --> 공부 필요 제 프로젝트 같은 경우에는 트래픽이 부담될만큼 발생될 여지가 (거의..) 잘 없다고 판단되어 세션 기반의 인증 방식 또한 적합하다고 생각했습니다. 하지만 실무에서는 위와 같은 고려 사항 또한 중요하기에 학습이 필요하다고 생각합니다.
인프런 김영한님 강의 - 스프링 MVC 2편
도전하는 개발자