ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • [Spring] 로그인 세션 관리
    Programming/SpringBoot 2024. 5. 21. 17:22
    728x90

    세션

    Session

    세션은 서버에 값을 저장한다.

    서버는 세션을 사용해서 클라이언트의 상태를 유지할 수 있다.

     

    세션 관리는 왜 필요할까?

    웹은 HTTP 프로토콜을 사용한다.

    HTTP는 connectionless, stateless 특징을 가지고 있다.

    비연결성, 서버가 클라이언트의 상태를 저장하지 않는다는 것이다.

    어떤 사용자가 어떤 세션을 사용하고 있는지 알 수 없기 때문에 세션 관리가 필요하다.

    즉, 로그인한 사용자 정보를 유지하기 위한 목적으로 세션을 사용한다.

     

    session의 동작 과정

    Spring Boot는 기본 서블릿 컨테이너로 Tomcat을 사용한다.

    Tomcat의 경우 세션 id로 JSESSIONID를 사용한다.

    https://raonctf.com/essential/study/web/session_connection

     

    서버에서 세션을 만드는 시점은?

    클라이언트가 요청하면 

    서버에서 세션 아이디가 없을 때 세션을 만들어서 저장하고 Set-Cookie헤더에 세션 아이디(JSESSION)을 담아서 응답을 보낸다. 

    클라이언트는 세션 아이디를 쿠키에 저장한다.(로컬, 디스크)

     

    클라이언트는 다음 요청때 JSESSION을 Cookie헤더에 담아 서버에 요청을 보낸다.

    서버는 해당 JSESSION의 세션 ID를 통해 세션 정보를 찾게 된다.

     

    -> 세션을 사용하지만 쿠키도 사용해야하는 이상한 구조가 되었다. (편의성을 위해 쿠키를 사용하게 되었다.)

     

    HTTP Cookie헤더

    ● 요청 헤더 : Cookie

       HTTP요청 시 클라이언트에서 서버로 전달하는 쿠키 헤더

    ●  응답 헤더 : Set-Cookie

       서버에서 클라이언트로 전달하는 쿠키 헤더

     

     

    session의 발급

    Spring Boot는 기본 서블릿 컨테이너로 Tomcat을 사용한다.

    Tomcat은 세션 ID로 JSESSIONID를 사용한다.

     

    HttpServletRequest 클래스의 getSession() 메소드를 사용하여 HttpSession을 받아올 수 있다.

    ●  getSession(true)  : HttpSession이 존재하면 현재 HttpSession을 반환하고 존재하지 않으면 새로운 세션을 생성한다.

    ● getSession(false) : HttpSession이 존재하면 현재 HttpSession을 반환하고 존재하지 않으면 새로 생성하지 않고 null 반환한다.

     

     

     

    ServletContext

    서브릿 컨테이너 ( ex, 톰캣) 이 시작되면, 해당 서블릿 컨테이너는 모든 웹 애플리케이션들을 배포하고 로드한다.

    웹 애플리케이션이 로드되면 서블릿 컨테이너는 ServletContext를 한 번 생성하여, 서버 메모리에 보관한다.

     

    HttpServletRequestHttpServletResponse

    서블릿 컨테이너는 특정 포트(8080, 80)환경에서 HTTP Request를 받는 (listens) 웹 서버에 연결된다.

    클라이언트(웹 브라우저를 가진 사용자)가 Http request를 보낼 때, 서블릿 컨테이너는 새로운 HttpServletRequest와 HttpServletResponse 인스턴스를 생성하고 해당 인스턴스를 미리 정의된 필터 체인과 서블릿 인스턴스를 통과하도록 한다.

    필터의 경우 doFilter() 메소드가 호출된다. chain.doFilter(request, response)를 호출하면 request와 response가 다음 필터로 넘어가거나, 남아있는 필터가 없는 경우 서블릿에 도달한다.

    서블릿에 경우 service()메소드가 호출된다. 기본적으로 이 메소드는 request.getMethod()메소드를 기반으로 doGet, doPost 등 메소드 중 하나를 호출한다. 해당되는 메소드가 서블릿에 없으면 응답에 HTTP 405 에러가 리턴된다. 

     

    request객체는 header와 body같은 HTTP request에 대한 모든 정보를 가지고 있다.

    response객체는 header와 body를 설정하여 원하는 방식으로 HTTP응답을 보낼 수 있다.

     

    HTTP 응답이 완료되면 request객체와 response객체는 모두 재활용되어 재사용 할 수 있다.

     

    HttpSession

    클라이언트가 처음으로 웹 어플리케이션을 방문하거나 request.getSession()을 통해 HttpSession을 처음으로 가져오면 서블릿 컨테이너는 새로운 HttpSession객체를 생성하고 길고 Unique한 ID를 생성 후, 서버의 메모리에 저장한다. (session.getId()를 통해 가져올 수 있다.)

    또한 서블릿 컨테이너는 JSESSIONID를 key, sessionID를 value로 생성하여 HTTP응답의 Set-Cookie header에 cookie로 설정한다.

     

    서블릿 컨테이너는 들어오는 모든 HTTP request의 cookie header에서 JSESSIONID라는 이름의 cookie가 있는지 확인하고 해당 값 (Session ID)를 사용하여 서버의 메모리에 저장된 HttpSession을 가져온다.

     

    HttpSession은 web.xml의 설정인 <session-timeout>에 지정된 값 까지만 살아있다.

    기본값은 30분이다. 따라서 클라이언트가 time out보다 오래 웹 어플리케이션을 방문하지 않으면 서블릿 컨테이너가 session을 삭제한다. 

    모든 request는 지정된 cookie가 있더라도 더 이상 동일한 session에 접근할 수 없으며, 서블릿 컨테이너는 새로운 session을 생성할 것이다.

     

    클라이언트 측에서 웹 브라우저 인스턴스가 실행되는 동안 session cookie가 활성화된다.

    따라서 클라이언트가 웹 브라우저 인스턴스(모든 탭/창)을 닫으면 클라이언트 session이 삭제된다.

     

    ServletContext는 웹 애플리케이션이 살아있는한 계속 살아있다.

    그리고 ServletContext는 모든 session에서 모든 request간에 공유된다.

    클라이언트가 동일한 브라우저 인스턴스로 웹 애플리케이션과 상호 작용하고 session이 서버에서 time out되지 않는 한 HttpSession은 계속 유지된다.

    같은 session은 모든 request간에 공유된다.

     

    Thread Safety

    스레드 안정성

    서블릿과 필터는 모든 request에서 공유된다.

    이것은 Java가 멀티 스레드와 다른 스레드(Http request)는 동일한 인스턴스를 사용할 수 있다는 점을 알수 있다.

    동일한 인스턴스를 사용하지 않으면 매 request마다 init() 및 destory()를 다시 실행해야 하는 비용이 든다.

    request나 session에서 사용하는 데이터를 서블릿이나 필터의 인스턴스 변수로 할당해서는 안된다.

    다른 session의 모든 request간에 공유되어 스레드로부터 안전하지 않다.

    public class ExampleServlet extends HttpServlet {
    
        private Object thisIsNOTThreadSafe; //쓰레드에 안전하지 않은 변수
    
        protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
            Object thisIsThreadSafe; // 쓰레드에 안전한 지역변수
    
            thisIsNOTThreadSafe = request.getParameter("foo"); // BAD!! 모든 request가 공유합니다.
            thisIsThreadSafe = request.getParameter("foo"); // OK, 이건 쓰레드에 안전합니다.
        }
    }

     

    HttpSession은 Servlet Container에서 만든다! 서블릿 컨테이너란 톰캣과 같은 WAS이다.

    HttpSession은 서블릿 컨테이너에서 생성한 인스턴스이다.

     

    Servlet Container에서는 Request에 SessionID가 있으면 Session을 새로 발급하지 않고 전달받은 SessionID와 매핑되는 기존 Session을 할당한다.

     

    서버에서 로그인 정보가 일치하면 세션을 생성해주고, 그 세션에 key-value형식으로 로그인 정보를 저장한다.(setAttrubute)

     

    구현

    Spring에서 세션을 가져오는데 주로 3가지 방법을 사용한다.

    1) HttpServletRequest에서 직접 가져오기

    2) SessionUtils 를 만들어 RequestContextHolder에서 가져오기

    3) 어노테이션 기반으로 읽기(SessionAttribute)


    1번 방법은 Java에서 HttpServletRequest에 대한 접근은 제한이 있다

    servlet, filter, interceptor, AOP, controller 정도에서만 접근이 가능하다.

    만약 service에서 사용하고 싶다면 controller에서 service로 파라미터로 전달해야 한다.


    2번 방법

    RequestContextHolder는 Spring2.x부터 제공되는 기능으로 Controller, service, DAO 전 구간에서 HttpServletRequest에 접근할 수 있는 유틸성 클래스이다.

     

    원리를 간단하게 설명하자면,

    Spring은 servlet이 호출되면 ThreadLocalMap이라는 객체에 key를 thread로 해서 제공받은 HttpServletRequest를 보관한다. (ServletRequestAttributes로 wrapping해서 보관)그리고 servlet이 종료될 때 해당 thread를 key로 갖는 HttpServletRequest을 map에서 제거한다.

    그 결과, 호출된 servlet과 동일한 thread내에서는 어느 곳에서든 같은 HttpServletRequest를 꺼내 쓸 수 있게 된다.

    그리고 controller에서 추가적인 작업을 하지 않아도 service, DAO 등에서 HttpServletRequest에 접근이 가능해진다. 

     

    RequestContextHolder 클래스가 초기화되는건 Servlet이 생성될 때이다.

    즉, Http Request가 오는 시점에 생성 및 초기화가 되어지고 Business layer를 거친 뒤 Servlet이 destory될 때 clean되고 있다. 

    RequestContextHolder 는 추상 클래스로 구현되어 있다.

    public abstract class RequestContextHolder {
        private static final boolean jsfPresent = ClassUtils.isPresent("jakarta.faces.context.FacesContext", RequestContextHolder.class.getClassLoader());
        private static final ThreadLocal<RequestAttributes> requestAttributesHolder = new NamedThreadLocal("Request attributes");
        private static final ThreadLocal<RequestAttributes> inheritableRequestAttributesHolder = new NamedInheritableThreadLocal("Request context");
    ....
    }

    내부 필드를 보면 static으로 선언되었기에 클래스 생성과 동시에 만들어지고, Servlet이 요청/종료될 때 마다 일일히 값을 채워 넣고, 없애는 작업을 한다.

     

    Http요청이 오게 되면 FrameworkServlet클래스의 processRequest라는 메서드가 호출된다.

    해당 메서드안에서 requestAttribute가 남아있는지 확인하고 새로운 requestAttributes를 만든다.

    그리고 iniContextHolders() 메소드로 ContextHolder를 초기화시킨다.

     

     

    FrameworkServlet (Spring Framework 6.1.7 API)

    setContextConfigLocation public void setContextConfigLocation(@Nullable String contextConfigLocation) Set the context config location explicitly, instead of relying on the default location built from the namespace. This location string can consist of mu

    docs.spring.io

    -> Request Thread, 즉 Tomcat Thread에서는 어디서든 static한 값을 꺼내쓸 수 있게 되는 것이다.

     

    애플리케이션을 실행하면 Tomcat이 실행되며 스레드가 하나 생성되면서 Servlet을 서빙한다.

    @Component, @Service, @Repository 등은 Business layer로 Spring Container에 등록되어 같은 스레드에서 동작하므로 잘 동작된다.

     

    동작원리

    static한 ThreadLocal에 값을 Write/Read하는 방식이다. 그렇기 때문에 같은 스레드에서는 값을 꺼내고 쓸 수 있다고 하는 것이다.

    그러나 다른 스레드(new Thread, 혹은 executor를 사용한 ThreadPool에서의 참조 등) 에서는 RequestContextHolder의 Request값을 꺼내 쓸 수 없다.

    왜냐면 새로운 스레드를 생성하는 순간 DispatcherServlet의 범위에서 벗어나서 새로운 스레드가 생성되기 때문이다.

    (Spring Container밖의 스레드가 생성되었기 때문에 참조를 하지 못한다)


    3번 방법

    어노테이션 기반으로 읽기, @SessionAttribute

     

    전역적으로 관리되며 Model정보를 HTTP 세션에 저장해주는 어노테이션이다.

     

    @ModelAttribute 또는 model.addAttribute()를 활용하여 객체를 저장할 경우 세션에 저장됩니다.

     

     

     

     

     

    참고

     

    https://yejipro.tistory.com/entry/javalangIllegalArgumentException-Unknown-return-value-type-javalangBoolean

     

    https://stir.tistory.com/m/432

     

    https://jojoldu.tistory.com/118

     

    https://oingdaddy.tistory.com/400

     

    https://blog.leaphop.co.kr/blogs/38/Multi_thread%EC%97%90%EC%84%9C_RequestContextHolder_%EC%A0%84%EB%8B%AC%ED%95%B4%EC%84%9C_%EC%82%AC%EC%9A%A9%ED%95%98%EB%8A%94_%EB%B0%A9%EB%B2%95

     

    https://raonctf.com/essential/study/web/session_connection

     

    https://dveamer.github.io/backend/SpringRequestContextHolder.html

     

     

    https://gompangs.tistory.com/entry/Spring-RequestContextHolder   

       

    https://docs.spring.io/spring-framework/docs/current/javadoc-api/org/springframework/web/servlet/FrameworkServlet.html

     

    https://parkmuhyeun.github.io/woowacourse/2023-05-05-Filter-Interceptor/

     

     

    728x90

    댓글

© 2022. code-space ALL RIGHTS RESERVED.