베어_
TechBear
베어_
전체 방문자
오늘
어제
  • 분류 전체보기 (336)
    • Spring (33)
      • 개념 (13)
      • Security (5)
      • 실습 (1)
      • 토비 스프링 (11)
    • JPA (6)
    • 프로젝트 기록 (24)
    • DB (13)
    • JAVA (18)
    • 알고리즘 (50)
      • 유형정리 (8)
      • Baekjoon (21)
      • LeetCode (18)
    • 디자인패턴 (0)
    • 개발서적 (79)
      • Effective Java (78)
      • 객체지향의 사실과 오해 (1)
    • 독후감 (4)
    • 보안 (2)
    • 운영체제(OS) (53)
      • 공룡책 (53)
    • 컴퓨터 네트워크 (28)
      • 컴퓨터 네트워크 하향식 접근 (23)
    • 자료구조 (1)
    • DevOps (2)
    • 앱 개발 (20)
      • 안드로이드 스튜디오 (20)

블로그 메뉴

    공지사항

    인기 글

    태그

    • 이펙티브자바
    • 스레드
    • 함수형인터페이스
    • 스프링
    • java
    • dfs
    • 백준
    • 코드업
    • Spring
    • 데이터베이스
    • 알고리즘
    • BFS
    • jpa
    • 운영체제
    • 스프링시큐리티
    • 자바8
    • 자바
    • leetcode
    • C++
    • 토비스프링

    최근 댓글

    최근 글

    티스토리

    hELLO · Designed By 정상우.
    베어_

    TechBear

    프로젝트 기록

    스프링 세션 - 이중 발급과, 인코딩된 세션

    2023. 12. 13. 23:11

    🔍 트러블 슈팅

    ✏️Key가 두 개 생성되는 문제

    세션을 저장하는 로직은 다음과 같다.

    @Component  
    class SessionFilter(  
        private val redisSessionRepository: RedisSessionRepository,  
    ): OncePerRequestFilter() {  
      
        val log = logger()  
      
        override fun doFilterInternal(  
    	    request: HttpServletRequest,  
    	    response: HttpServletResponse,  
    	    filterChain: FilterChain  
    	) {  
      
    	    val session = getSessionFromCookie(request)          --- 1
    	    if (session == null) {  
    	        val sessionId = request.getSession(true).id      --- 2
    	        myRedisSessionRepository.setSession(sessionId)   --- 3
    	    }  
      
    	    filterChain.doFilter(request, response)  
    	}
    }

    1. 쿠키에 세션에 대한 정보가 있는지 확인한다.
    2.세션이 없다면 새로운 세션을 생성한다.
    3.새로 발급받은 세션을 레디스에 저장한다.

    결과를 보면 세션이 두 개가 저장되었음을 확인할 수 있다. 첫 번째는 위의 코드에 의해 발급된 것이고 2번 세션은 왜 발생한 것일까?

    Screenshot 2023-12-09 at 18.03.03.png

    이를 해결하기 위해 디버깅 모드로 코드를 하나씩 따라 가 봤다.

    1. getSession()을 호출하면 HttpServletRequestWrapper의 getSession이 호출된다.
    class HttpServletRequestWrapper {
    	@Override  
    	public HttpSession getSession(boolean create) {  
    	    return this._getHttpServletRequest().getSession(create);  
    	}
    }

     

    1. SessionRepositoryFilter가 동작한다.
    @Override  
    public HttpSessionWrapper getSession(boolean create) {  
    	 // 1. Session이 있는지 확인합니다.
        HttpSessionWrapper currentSession = getCurrentSession();  
        if (currentSession != null) {  
           return currentSession;  
        }  
    
    	 // 2. Session을 생성하고 등록합니다.
        S session = SessionRepositoryFilter.sessionRepository.createSession();  
        session.setLastAccessedTime(Instant.now());  
        currentSession = new HttpSessionWrapper(session, getServletContext());  
        setCurrentSession(currentSession);  
        return currentSession;  
    }

     

    1. RedisSessionRepository가 동작한다.
    public RedisSessionRepository {
    	@Override  
    	public RedisSession createSession() {  
    	    MapSession cached = new MapSession();  
    	    cached.setMaxInactiveInterval(this.defaultMaxInactiveInterval);  
    	    RedisSession session = new RedisSession(cached, true);  
    	    session.flushIfRequired();  // 
    	    return session;  
    	}
    }

    여기서 flush모드가 yes라면 곧바로 캐시에 업데이트 된다… 아니라면 응답을 보내기 전에 DispatcherServlet에서 save() 메서드를 호출하여 저장하는 작업을 거친다. 즉, 세션을 새로 발급하면 SessionRepositoryFilter가 동작하여 Redis에 세션을 저장해준다.

    ✏️ 쿠키와 Base64 인코딩

    웹 브라우저에서 확인해보면 SESSION이 인코딩되어서 들어간 것을 확인할 수 있다. (세션이 두 개 있는데 위의 글을 읽으면 해결할 수 있다.)

    Screenshot 2023-12-09 at 20.25.01 1.png

    configuration에서 session 생성을 false로 두면 된다고 하는데 잘 동작 되지 않았다.(스프링이 자동으로 넣어주는 기능을 끄니까 아마 제대로 동작했다면 위의 문제도 같이 해결되었을 것이다.) 다음 링크에서 힌트를 얻어 DefaultCookieSerializer부터 디버깅 모드로 하나씩 찾아나갔다.
    https://stackoverflow.com/questions/51517246/whats-the-difference-between-cookie-session-and-session-id-in-database-for-spri

    val sessionId = request.getSession(true).id 다음 코드를 호출하는 순간 RedisSessionRepository에서 세션을 생성하고 이를 현재 세션으로 넣은 다음 헤더에 추가해준다. 즉, 추가적으로 header에 담는 로직이 필요 없다. 실제로 세션이 2개가 들어왔지만 하나는 base64로 인코딩 한 값이다.

    이를 추적하면서 뽑아낸 중요한 코드 리스트다.

    1. Session이 있는지 확인한다.
    @Override  
    public HttpSessionWrapper getSession(boolean create) {  
        HttpSessionWrapper currentSession = getCurrentSession();  
    
    	// 현재 세션이 존재하면 return 
        if (currentSession != null) {  
           return currentSession;  
        }  
        
    	// 캐시되어 있는 세션이 있는지 확인
        S requestedSession = getRequestedSession();  
        if (requestedSession != null) {  
           if (getAttribute(INVALID_SESSION_ID_ATTR) == null) {  
              requestedSession.setLastAccessedTime(Instant.now());  
              this.requestedSessionIdValid = true;  
              currentSession = new HttpSessionWrapper(requestedSession, getServletContext());  
              currentSession.markNotNew();  
              setCurrentSession(currentSession);  
              return currentSession;  
           }  
        }
    
    	// create의 속성이 fasle라면 세션을 생성하지 않는다.
    	if (!create) {  
    	    return null;  
    	}
    
    	// create = true라면 새로운 세션을 생성한다.
    	S session = SessionRepositoryFilter.this.sessionRepository.createSession();  
    	session.setLastAccessedTime(Instant.now());  
    	currentSession = new HttpSessionWrapper(session, getServletContext());  
    	setCurrentSession(currentSession);  
    	return currentSession;
    }

     

    1. 지정한 스토리지에서 Session을 생성한다. (flush모드에 따라 저장 시점이 다르다)
    @Override  
    public RedisSession createSession() {  
        MapSession cached = new MapSession();  
        cached.setMaxInactiveInterval(this.defaultMaxInactiveInterval);  
        RedisSession session = new RedisSession(cached, true);  
        session.flushIfRequired();  
        return session;  
    }

     

    1. 이후 Response 보내기 전에 세션을 Base64로 인코딩하여 header에 저장한다.
    @Override  
    public void setSessionId(HttpServletRequest request, HttpServletResponse response, String sessionId) {  
        if (sessionId.equals(request.getAttribute(WRITTEN_SESSION_ID_ATTR))) {  
           return;  
        }  
        request.setAttribute(WRITTEN_SESSION_ID_ATTR, sessionId);  
        this.cookieSerializer.writeCookieValue(new CookieValue(request, response, sessionId));  
    }
    @Override  
    public void writeCookieValue(CookieValue cookieValue) {  
        String value = getValue(cookieValue);   // getValue()에서 인코딩함.
    	...
        response.addHeader("Set-Cookie", sb.toString());  // 헤더에 저장
    }

     

    ✏️ 결론

    1. 스프링 세션을 적용하면 session이 생성되는 시점에 데이터를 저장해준다.
    2. 세션을 쿠키로 반환할 때는 Base64로 인코딩하면 반환한다.
      '프로젝트 기록' 카테고리의 다른 글
      • 디자인패턴을 이용한 장바구니 기능 구현 - 레디스와 스프링부트 활용
      • 스프링 세션 적용하기 (feat. filter)
      • 스프링 세션의 이해 - 등장 배경과 그 원리
      • 2차 캐시를 이용한 최적화 기록
      베어_
      베어_
      Today I learned | 문제를 해결하는 개발자

      티스토리툴바