Spring

서블릿 HTTP 세션

dev-rootable 2023. 7. 27. 16:17

📌 세션이 필요한 이유

 

1. HTTP는 비연결성 특성을 갖고 있어 사용자를 식별하지 못한다. 즉, 별도 처리 없이 사용자의 정보를 유지하지 않는다.

 

2. 1번 문제점을 해결하기 위해 쿠키를 선택할 수 있다. 하지만 쿠키는 다음과 같은 문제점이 있다.

 

  • 쿠키 값은 클라이언트에서 임의로 변경할 수 있다.
  • 쿠키에 보관된 정보는 훔쳐갈 수 있고, 한번 훔쳐가면 평생 사용할 수 있다.
  • 쿠키 값은 단순하고 규칙성이 있어 보안면에서 취약하다.

 

쿠키 값 변경

 

쿠키 값 예시

 

🔎 대안

 

클라이언트와 서버가 서로를 식별하기 위해 쿠키를 써야 하는 점은 동일하다. 하지만 다음과 같은 조건을 충족시킨다면 보안 측면에서 안전하게 통신할 수 있다.

 

  • 쿠키에 중요한 정보를 담지 않는다.
  • 쿠키로 전달되는 세션 ID(식별자)는 사용자 별로 예측 불가능한 임의의 토큰(랜덤값)이어야 한다.
  • 서버에서 해당 토큰의 만료 시간을 짧게 유지한다. (보통 30분)

 

💡 세션(Session)이란?

서버에 중요한 정보를 보관하고 클라이언트-서버 간 연결을 유지하는 방법

 

📌 세션 동작 방식

 

1. 사용자가 로그인 ID와 패스워드를 전달하면 서버에서 해당 사용자가 맞는지 확인

 

로그인 후 정보 전달

 

2. 자바의 UUID(추정 불가능)를 통해 세션 ID를 생성한다. 해당 세션 ID를 key, 회원 객체를 value로 세션 저장소에 보관

 

세션 저장

 

3. 세션 ID를 응답 쿠키로 전달한다. 예를 들어 "mySession"이라는 이름으로 세션 ID(UUID)만 쿠키에 담아서 전달한다. 그 후에 클라이언트는 쿠키 저장소에 mySession 쿠키를 보관하고, 요청마다 이 값을 포함한다.

 

세션 ID 전달

 

4. 서버에서는 클라이언트가 전달한 mySession 쿠키 정보로 세션 저장소를 조회해서 로그인 시 보관한 세션 정보(회원 정보)를 사용한다.

 

세션 정보 사용

 

5. 일정 시간 후 세션이 만료되어 저장소에 유지되었던 정보가 제거된다.

 

📌 서블릿 HTTP 세션

 

서블릿을 통해 HttpSession을 생성하면 다음과 같은 쿠키를 생성한다.

 

  • 쿠키 이름 : JSESSIONID
  • 값 : 추정 불가능한 랜덤 값

 

HttpSession을 통해 세션 생성, 조회, 소멸까지 구현할 수 있다.

 

@Slf4j
@Controller
@RequiredArgsConstructor
public class LoginController {

    private final LoginService loginService;
    private final SessionManager sessionManager;

    @GetMapping("/login")
    public String loginForm(@ModelAttribute("loginForm") LoginForm form) {
        return "login/loginForm";
    }

    @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());
        log.info("login? {}", loginMember);

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

        //로그인 성공 처리

        //세션이 있으면 있는 세션 반환, 없으면 신규 세션 생성
        HttpSession session = request.getSession();
        //세션에 로그인 회원 정보 보관
        session.setAttribute(SessionConst.LOGIN_MEMBER, loginMember);

        return "redirect:/";

    }

    @PostMapping("/logout")
    public String logout(HttpServletRequest request) {
        //세션을 삭제한다.
        HttpSession session = request.getSession(false);
        if (session != null) {
            session.invalidate(); //세션 삭제
        }

        return "redirect:/";
    }

}

 

🔎 getSession

 

HttpServletRequest를 통해 세션 정보를 조회할 수 있다. "getSession(boolean create)"을 사용하면 기존 세션이 있는지 확인한 후 없으면 새로운 세션을 생성해서 반환한다.

 

  • getSession(true) : 세션이 있으면 기존 세션을 반환, 없으면 새로운 세션을 생성해서 반환
  • getSession(false) : 세션이 있으면 기존 세션을 반환, 없으면 새로운 세션을 생성하지 않고 null 반환

 

🔎 저장된 세션 사용

 

세션에 저장된 값이 있는지 여부를 통해 로그인 상태인지 확인할 수 있다.

 

다음 코드는 세션 정보가 있으면 로그인한 회원용 화면을 렌더링 하고 세션에 정보가 없는 회원은 로그인을 하지 못하도록 한다.

 

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

        //세션이 없으면 home
        HttpSession session = request.getSession(false);
        if (session == null) {
            return "home";
        }

        Member loginMember = (Member) session.getAttribute(SessionConst.LOGIN_MEMBER);
        //세션에 회원 데이터가 없으면 home
        if (loginMember == null) {
            return "home";
        }

        //세션이 유지되면 로그인으로 이동
        model.addAttribute("member", loginMember);
        return "loginHome";

    }

 

여기서는 로그인 유무만 판단하므로 세션을 다시 생성할 이유가 없다. 그래서 getSession의 매개 변수는 false다. 그렇다면, 리턴 값은 null이 가능하고, null일 경우 로그인하지 않은 사용자로 간주한다.

 

getAttribute를 통해 세션에 저장된 객체를 가져왔다. 해당 객체가 null일 경우는 필터링하고 Model을 통해 템플릿으로 객체를 넘겨 렌더링 한다.

 

서버에서 응답으로 내린 JSESSIONID

 

클라이언트에서 재사용한 JSESSIONID

 

로그인 전(좌), 로그인 후(우)

 

📌 @SessionAttribute

 

스프링은 세션을 조회하고, 세션에 들어 있는 데이터를 찾아오는 과정을 한 번에 편리하게 처리해 준다.

 

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

        //세션에 회원 데이터가 없으면 home
        if (loginMember == null) {
            return "home";
        }

        //세션이 유지되면 로그인으로 이동
        model.addAttribute("member", loginMember);
        return "loginHome";

    }

 

참고로 "required = false"는 해당 컨트롤러를 실행하는데 필수 값이 아니라는 것이다. 즉, 세션이 없는 사용자도 홈 화면에 접근할 수 있다.

 

📌 세션 정보 확인

 

@Slf4j
@RestController
public class SessionInfoController {

    @GetMapping("/session-info")
    public String sessionInfo(HttpServletRequest request) {

        HttpSession session = request.getSession(false);
        if (session == null) {
            return "세션이 없습니다.";
        }

        //세션 데이터 출력
        session.getAttributeNames().asIterator()
                .forEachRemaining(name -> log.info("session name={}, value={}", name, session.getAttribute(name)));

        log.info("sessionId={}", session.getId());
        log.info("maxInactiveInterval={}", session.getMaxInactiveInterval());
        log.info("creationTime={}", session.getCreationTime());
        log.info("lastAccessedTime={}", session.getLastAccessedTime());
        log.info("isNew={}", session.isNew());

        return "세션 출력";

    }

}

 

  • sessionId : 세션 ID, JSESSIONID의 값
  • maxInactiveInterval : 세션의 유효 시간
  • creationTime : 세션 생성일시
  • lastAccessedTime : 세션과 연결된 사용자가 최근에 서버에 접근한 시간, 클라이언트에서 서버로 sessionId(JSESSION)를 요청한 경우에 갱신된다.
  • isNew : 새로 생성된 세션인지, 아니면 이미 과거에 만들어졌고, 클라이언트에서 서버로 sessionId(JSESSIONID)를 요청해서 조회된 세션인지 여부

 

세션 정보

 

📌 세션의 용량과 타임아웃

 

1. 세션은 기본적으로 메모리에 생성된다. 메모리의 크기가 무한하지 않기 때문에 꼭 필요한 경우에만 생성해서 사용해야 한다.

 

2. 세션은 사용자 수 * 보관한 데이터 용량만큼 메모리를 차지하므로, 사용량이 급격하게 늘어나서 장애로 이어지지 않도록 최소한의 데이터만 보관해야 한다.

 

 - 결국 쿠키를 보내기 때문에 개인 정보와 같은 중요 정보를 포함해선 안된다.

 

3. HTTP는 비연결성이므로 브라우저의 종료를 인지할 수 없다. 그래서 세션 데이터를 언제 삭제해야 할지 판단하기 어렵다.

 

 - 세션 타임아웃을 설정하여 일정 시간 후 삭제되도록 해야 한다. 이렇게 하면 세션을 탈취당했을 때도 악의적인 요청 피해를 감소시킬 수 있다.

 

💡 스프링 세션 타임아웃 설정(application.properties)

server.servlet.session.timeout=60 (60초) //최근에 서버에 요청한 시간 기준

특정 세션 단위로 시간 설정을 하려면

session.setMaxInactiveInterval(1800); //1800초

 

Reference:

https://www.inflearn.com/course/%EC%8A%A4%ED%94%84%EB%A7%81-mvc-2/dashboard

 

스프링 MVC 2편 - 백엔드 웹 개발 활용 기술 - 인프런 | 강의

웹 애플리케이션 개발에 필요한 모든 웹 기술을 기초부터 이해하고, 완성할 수 있습니다. MVC 2편에서는 MVC 1편의 핵심 원리와 구조 위에 실무 웹 개발에 필요한 모든 활용 기술들을 학습할 수 있

www.inflearn.com