@@ -32,28 +32,35 @@ protected void doFilterInternal(@Nullable HttpServletRequest request, @Nullable
3232 throws ServletException , IOException {
3333
3434 if (request != null && response != null ) {
35- // 1. Authorization 헤더에서 Bearer 토큰 추출 시도 (우선순위 1)
36- String accessToken = extractTokenFromAuthorizationHeader (request );
37- boolean fromHeader = accessToken != null ;
38-
39- // 2. Authorization 헤더에 없으면 쿠키에서 토큰 추출 (우선순위 2)
40- if (accessToken == null ) {
41- accessToken = cookieUtil .getAccessTokenFromCookies (request );
42- }
43-
44- // JWT 액세스 토큰 검증 및 인증 처리
45- if (accessToken != null ) {
46- TokenProvider .TokenValidationResult validationResult = tokenProvider .validateTokenWithResult (accessToken );
47-
48- if (validationResult == TokenProvider .TokenValidationResult .VALID ) {
49- // 유효한 토큰인 경우 인증 처리
50- setAuthentication (accessToken );
51- } else if (validationResult == TokenProvider .TokenValidationResult .EXPIRED && !fromHeader ) {
52- // 만료된 토큰이고 쿠키에서 왔을 경우에만 자동 갱신 시도
53- // (Authorization 헤더 토큰은 클라이언트가 직접 관리해야 함)
54- tryAutoRefreshToken (request , response , accessToken );
35+ try {
36+ // 1. 쿠키에서 액세스 토큰 확인
37+ String accessToken = cookieUtil .getAccessTokenFromCookies (request );
38+
39+ if (accessToken != null ) {
40+ // 액세스 토큰이 있는 경우 검증
41+ TokenProvider .TokenValidationResult validationResult = tokenProvider .validateTokenWithResult (accessToken );
42+
43+ if (validationResult == TokenProvider .TokenValidationResult .VALID ) {
44+ // 유효한 액세스 토큰 - 인증 처리
45+ setAuthentication (accessToken );
46+ log .debug ("유효한 액세스 토큰으로 인증 완료" );
47+ } else if (validationResult == TokenProvider .TokenValidationResult .EXPIRED ) {
48+ // 만료된 액세스 토큰 - 리프레시 토큰으로 갱신 시도
49+ log .info ("액세스 토큰 만료, 리프레시 토큰으로 갱신 시도" );
50+ handleTokenRefresh (request , response , accessToken );
51+ } else {
52+ // 유효하지 않은 액세스 토큰 - 리프레시 토큰 확인
53+ log .warn ("유효하지 않은 액세스 토큰, 리프레시 토큰으로 갱신 시도" );
54+ handleTokenRefresh (request , response , null );
55+ }
56+ } else {
57+ // 4. 액세스 토큰이 없는 경우 바로 리프레시 토큰 확인
58+ log .debug ("액세스 토큰이 없음, 리프레시 토큰 확인" );
59+ handleTokenRefresh (request , response , null );
5560 }
56- // INVALID인 경우 아무 처리 하지 않음 (인증되지 않은 상태로 진행)
61+ } catch (Exception e ) {
62+ log .error ("JWT 인증 처리 중 오류 발생: {}" , e .getMessage (), e );
63+ clearAuthenticationAndCookies (response );
5764 }
5865 }
5966
@@ -62,19 +69,6 @@ protected void doFilterInternal(@Nullable HttpServletRequest request, @Nullable
6269 }
6370 }
6471
65- /**
66- * Authorization 헤더에서 Bearer 토큰을 추출합니다.
67- * @param request HTTP 요청
68- * @return Bearer 토큰 값 또는 null
69- */
70- private String extractTokenFromAuthorizationHeader (HttpServletRequest request ) {
71- String authHeader = request .getHeader ("Authorization" );
72- if (authHeader != null && authHeader .startsWith ("Bearer " )) {
73- return authHeader .substring (7 ); // "Bearer " 제거
74- }
75- return null ;
76- }
77-
7872 /**
7973 * JWT 토큰에서 사용자 정보를 추출하여 Spring Security 인증 객체를 설정합니다.
8074 * @param token JWT 액세스 토큰
@@ -104,60 +98,88 @@ private void setAuthentication(String token) {
10498 }
10599
106100 /**
107- * 만료된 액세스 토큰으로 자동 갱신을 시도합니다 .
108- * @param request HTTP 요청
109- * @param response HTTP 응답
110- * @param expiredAccessToken 만료된 액세스 토큰
101+ * 토큰 갱신을 처리합니다 .
102+ * 2. 액세스토큰이 만료되었으면 리프레시토큰을확인한다
103+ * 3. 리프레시토큰이 레디스의 저장값과 동일하면 토큰 재발급을 진행한다
104+ * 6. 리프레시토큰을 확인하는절차에서 리프레시토큰이 없을 경우 쿠키에 있는 모든 정보를 제거하고 로그인을 해달라고 메시지를 반환한다
111105 */
112- private void tryAutoRefreshToken (HttpServletRequest request , HttpServletResponse response , String expiredAccessToken ) {
106+ private void handleTokenRefresh (HttpServletRequest request , HttpServletResponse response , String expiredAccessToken ) {
113107 try {
114- // 1. 만료된 토큰에서 loginId 추출
115- String loginId = tokenProvider .getLoginIdFromExpiredToken (expiredAccessToken );
116- if (loginId == null ) {
117- log .warn ("만료된 토큰에서 loginId 추출 실패" );
108+ // 2. 리프레시 토큰 확인
109+ String refreshToken = cookieUtil .getRefreshTokenFromCookies (request );
110+ if (refreshToken == null ) {
111+ // 6. 리프레시 토큰이 없을 경우 쿠키 클리어
112+ log .info ("리프레시 토큰이 없음 - 쿠키 클리어 및 재로그인 필요" );
113+ clearAuthenticationAndCookies (response );
118114 return ;
119115 }
120116
121- // 2. 쿠키에서 리프레시 토큰 추출
122- String refreshToken = cookieUtil .getRefreshTokenFromCookies (request );
123- if (refreshToken == null ) {
124- log .info ("리프레시 토큰이 없어 자동 갱신 불가: {}" , loginId );
117+ // loginId 추출 시도 (만료된 토큰이 있으면 그것에서, 없으면 리프레시 토큰으로 찾기)
118+ String loginId = null ;
119+ if (expiredAccessToken != null ) {
120+ loginId = tokenProvider .getLoginIdFromExpiredToken (expiredAccessToken );
121+ }
122+
123+ // 만료된 토큰에서 추출 실패 시 리프레시 토큰으로 사용자 찾기
124+ if (loginId == null ) {
125+ loginId = tokenProvider .findUsernameByRefreshToken (refreshToken );
126+ }
127+
128+ if (loginId == null ) {
129+ log .warn ("loginId 추출 실패 - 쿠키 클리어" );
130+ clearAuthenticationAndCookies (response );
125131 return ;
126132 }
127133
128- // 3. 리프레시 토큰 유효성 검증
134+ // 3. 리프레시 토큰이 Redis의 저장값과 동일한지 검증
129135 if (!tokenProvider .validateRefreshToken (loginId , refreshToken )) {
130- log .info ("유효하지 않은 리프레시 토큰으로 자동 갱신 불가: {}" , loginId );
136+ log .info ("유효하지 않은 리프레시 토큰 - 쿠키 클리어: {}" , loginId );
137+ clearAuthenticationAndCookies (response );
131138 return ;
132139 }
133140
134- // 4. 회원 정보 조회
141+ // 회원 정보 조회
135142 Member member = memberRepository .findByLoginId (loginId ).orElse (null );
136143 if (member == null ) {
137- log .warn ("존재하지 않는 회원으로 자동 갱신 불가: {}" , loginId );
144+ log .warn ("존재하지 않는 회원 - 쿠키 클리어: {}" , loginId );
145+ clearAuthenticationAndCookies (response );
138146 return ;
139147 }
140148
141- // 5. RTR(Refresh Token Rotation) 패턴: 기존 리프레시 토큰 삭제
142- tokenProvider .deleteRefreshToken (loginId );
149+ // RTR(Refresh Token Rotation) 패턴: 기존 모든 토큰 삭제
150+ tokenProvider .deleteAllTokens (loginId );
143151
144- // 6. 새로운 액세스 토큰과 리프레시 토큰 생성
152+ // 새로운 액세스 토큰과 리프레시 토큰 생성
145153 String newAccessToken = tokenProvider .generateAccessToken (member );
146154 String newRefreshToken = tokenProvider .generateRefreshToken (member );
147155
148- // 7. 새로운 토큰들을 쿠키에 설정
156+ // 새로운 토큰들을 쿠키에 설정
149157 cookieUtil .setTokenCookies (response , newAccessToken , newRefreshToken );
150158
151- // 8. 새로운 액세스 토큰으로 인증 설정
159+ // 새로운 액세스 토큰으로 인증 설정
152160 setAuthentication (newAccessToken );
153161
154- log .info ("액세스 토큰 자동 갱신 성공: {}" , loginId );
162+ log .info ("토큰 자동 갱신 성공: {}" , loginId );
155163
156164 } catch (Exception e ) {
157- log .warn ("액세스 토큰 자동 갱신 실패: {}" , e .getMessage ());
165+ log .error ("토큰 갱신 처리 실패: {}" , e .getMessage (), e );
166+ clearAuthenticationAndCookies (response );
158167 }
159168 }
160169
170+ /**
171+ * 인증 정보와 쿠키를 모두 클리어합니다.
172+ */
173+ private void clearAuthenticationAndCookies (HttpServletResponse response ) {
174+ // Spring Security 인증 정보 클리어
175+ SecurityContextHolder .clearContext ();
176+
177+ // 쿠키 클리어
178+ cookieUtil .clearTokenCookies (response );
179+
180+ log .debug ("인증 정보 및 쿠키 클리어 완료" );
181+ }
182+
161183 /**
162184 * JWT 인증이 필요하지 않은 경로들을 필터링에서 제외합니다.
163185 * @param request HTTP 요청
@@ -170,7 +192,10 @@ protected boolean shouldNotFilter(HttpServletRequest request) {
170192 path .equals ("/api/auth/login" ) ||
171193 path .equals ("/api/auth/refresh" ) ||
172194 path .startsWith ("/api/public/" ) ||
195+ path .startsWith ("/api/redis-test/" ) ||
173196 path .startsWith ("/swagger-" ) ||
174- path .startsWith ("/v3/api-docs" );
197+ path .startsWith ("/v3/api-docs" ) ||
198+ path .equals ("/actuator/health" ) ||
199+ path .startsWith ("/h2-console" );
175200 }
176201}
0 commit comments