제공해주신 파일들을 바탕으로 "세션 만료 후 재로그인 시 403 (Forbidden) 토큰 불일치 에러"가 발생하는 원인을 종합적으로 분석하고 해결책을 제시해 드립니다.
1. 문제의 핵심 원인: 3-Way 충돌
현재 구조는 3개의 다른 컴포넌트가 각기 다른 시점에 세션과 CSRF 토큰을 관리하려 하면서 충돌하고 있습니다.
CsrfFilter(서블릿 필터): 가장 먼저 실행됩니다.POST /member/login요청을 받으면, 컨트롤러 실행 전에ensureTokenInSession을 호출해 새 세션(A)을 만들고 새 토큰(Token_A)을 세션에 저장합니다.LoginInterceptor(스프링 인터셉터):CsrfFilter다음에 실행됩니다. 이 인터셉터는"sessionMemberInfo"가 세션에 있는지 확인합니다. 하지만 이 시점엔 아직MemberController가 실행되지 않았으므로"sessionMemberInfo"는null입니다.MemberController(컨트롤러): 인터셉터 다음에 실행됩니다. 로그인 성공 시session.setAttribute("sessionMemberInfo", ...)를 호출합니다. 이때 스프링(또는 서블릿 컨테이너)은 **세션 고정 공격(Session Fixation)**을 방어하기 위해 기존 세션(A)을 무효화하고 **완전히 새로운 세션(B)**을 생성할 수 있습니다.
에러 발생 시나리오 (403 Forbidden):
세션 만료: 사용자가 페이지에 머무는 동안 서버 세션이 만료됩니다. (브라우저의
XSRF-TOKEN쿠키(Token_Old)는 남아있습니다.)동작 실패 (401): 사용자가 저장을 누르면(
POST /admin/..),CsrfFilter가 세션이 없어 401을 반환합니다. (이때LoginInterceptor가 먼저 401을 반환할 수도 있습니다.)로그인 시도:
loginSubmit.js가POST /member/login을 호출합니다.CsrfFilter가 요청을 받고,isLogin=true로 인지합니다.ensureTokenInSession이 실행되어 **새 세션(A)**을 만들고CSRF_TOKEN속성에 **Token_New**를 저장합니다.chain.doFilter로 다음 단계(인터셉터)로 넘깁니다.
컨트롤러 실행 및 세션 교체:
(
LoginInterceptor가/member/login을 제외(exclude)했다고 가정하고)MemberController가 실행됩니다.로그인 성공 후
session.setAttribute("sessionMemberInfo", ...)가 호출됩니다.이때 세션 고정 방어가 발동하여,
CsrfFilter가 만든 세션(A)이 파괴되고, 새로운 세션(B)이 생성됩니다."sessionMemberInfo"는 **세션(B)**에 저장됩니다.문제:
CsrfFilter가 세션(A)에 저장했던CSRF_TOKEN속성(Token_New)은 세션(B)로 복사되지 않고 소실됩니다.
토큰 동기화 실패:
MemberController가 종료되고CsrfFilter로 제어권이 돌아옵니다.syncTokenCookieIfSessionValid가 실행됩니다.request.getSession(false)는 이제 **세션(B)**를 반환합니다.session.getAttribute(CSRF_ATTR)를 호출하지만, **세션(B)**에는 이 속성이 없으므로null이 반환됩니다.결과적으로
Set-Cookie: XSRF-TOKEN=...헤더가 전송되지 않습니다.
403 발생:
브라우저는 여전히 만료된
XSRF-TOKEN쿠키(Token_Old)를 가지고 있습니다.사용자가 다시
POST /admin/..요청을 보냅니다.JS는
Token_Old를 헤더에 실어 보냅니다.CsrfFilter는 세션(B)(로그인된 유효한 세션)를 확인합니다.sessionToken = session.getAttribute(CSRF_ATTR)->null입니다.requestToken은Token_Old입니다.if (sessionToken == null)조건에 걸려 403 Forbidden을 반환합니다