11package com .ai .lawyer .global .jwt ;
22
3- import com .ai .lawyer .domain .member .entity .MemberAdapter ;
43import com .ai .lawyer .domain .member .repositories .MemberRepository ;
54import com .ai .lawyer .domain .member .repositories .OAuth2MemberRepository ;
65import jakarta .servlet .FilterChain ;
76import jakarta .servlet .ServletException ;
87import jakarta .servlet .http .HttpServletRequest ;
98import jakarta .servlet .http .HttpServletResponse ;
9+ import lombok .Getter ;
1010import lombok .extern .slf4j .Slf4j ;
1111import org .springframework .lang .Nullable ;
1212import org .springframework .security .authentication .UsernamePasswordAuthenticationToken ;
@@ -25,6 +25,7 @@ public class JwtAuthenticationFilter extends OncePerRequestFilter {
2525 private TokenProvider tokenProvider ;
2626 private CookieUtil cookieUtil ;
2727 private MemberRepository memberRepository ;
28+ @ Getter
2829 private OAuth2MemberRepository oauth2MemberRepository ;
2930
3031 public JwtAuthenticationFilter () {
@@ -56,14 +57,8 @@ public void setOauth2MemberRepository(OAuth2MemberRepository oauth2MemberReposit
5657 private static final String DEFAULT_ROLE = "USER" ;
5758
5859 // 로그 메시지 상수
59- private static final String LOG_TOKEN_EXPIRED = "액세스 토큰 만료, 리프레시 토큰으로 갱신 시도" ;
60- private static final String LOG_INVALID_TOKEN = "유효하지 않은 액세스 토큰, 리프레시 토큰으로 갱신 시도" ;
61- private static final String LOG_NO_REFRESH_TOKEN = "리프레시 토큰이 없음 - 쿠키 클리어 및 재로그인 필요" ;
62- private static final String LOG_LOGIN_ID_EXTRACTION_FAILED = "loginId 추출 실패 - 쿠키 클리어" ;
63- private static final String LOG_INVALID_REFRESH_TOKEN = "유효하지 않은 리프레시 토큰 - 쿠키 클리어: {}" ;
64- private static final String LOG_MEMBER_NOT_FOUND = "존재하지 않는 회원 - 쿠키 클리어: {}" ;
65- private static final String LOG_TOKEN_REFRESH_SUCCESS = "토큰 자동 갱신 성공: {}" ;
66- private static final String LOG_TOKEN_REFRESH_FAILED = "토큰 갱신 처리 실패: {}" ;
60+ private static final String LOG_TOKEN_EXPIRED = "액세스 토큰 만료 - 401 반환" ;
61+ private static final String LOG_INVALID_TOKEN = "유효하지 않은 액세스 토큰 - 401 반환" ;
6762 private static final String LOG_JWT_AUTH_ERROR = "JWT 인증 처리 중 오류 발생: {}" ;
6863 private static final String LOG_MEMBER_ID_EXTRACTION_FAILED = "토큰에서 memberId를 추출할 수 없습니다." ;
6964 private static final String LOG_SET_AUTH_FAILED = "인증 정보 설정 실패: {}" ;
@@ -93,7 +88,7 @@ protected void doFilterInternal(@Nullable HttpServletRequest request, @Nullable
9388 processAuthentication (request , response );
9489 } catch (Exception e ) {
9590 log .error (LOG_JWT_AUTH_ERROR , e .getMessage (), e );
96- clearAuthenticationAndCookies ( response );
91+ SecurityContextHolder . clearContext ( );
9792 }
9893 }
9994
@@ -115,21 +110,19 @@ private boolean shouldSkipFilter(HttpServletRequest request) {
115110 /**
116111 * 인증 프로세스를 처리합니다.
117112 */
118- private void processAuthentication (HttpServletRequest request , HttpServletResponse response ) {
113+ private void processAuthentication (HttpServletRequest request , HttpServletResponse response ) throws IOException {
119114 String accessToken = cookieUtil .getAccessTokenFromCookies (request );
120115
121116 if (accessToken != null ) {
122- handleAccessToken (request , response , accessToken );
123- } else {
124- // 액세스 토큰이 없는 경우 바로 리프레시 토큰 확인
125- handleTokenRefresh (request , response , null );
117+ handleAccessToken (response , accessToken );
126118 }
119+ // 액세스 토큰이 없는 경우 인증 처리하지 않음 (공개 API 허용)
127120 }
128121
129122 /**
130123 * 액세스 토큰을 검증하고 처리합니다.
131124 */
132- private void handleAccessToken (HttpServletRequest request , HttpServletResponse response , String accessToken ) {
125+ private void handleAccessToken (HttpServletResponse response , String accessToken ) throws IOException {
133126 TokenProvider .TokenValidationResult validationResult = tokenProvider .validateTokenWithResult (accessToken );
134127
135128 switch (validationResult ) {
@@ -138,18 +131,28 @@ private void handleAccessToken(HttpServletRequest request, HttpServletResponse r
138131 setAuthentication (accessToken );
139132 break ;
140133 case EXPIRED :
141- // 만료된 액세스 토큰 - 리프레시 토큰으로 갱신 시도
134+ // 만료된 액세스 토큰 - 401 반환
142135 log .info (LOG_TOKEN_EXPIRED );
143- handleTokenRefresh ( request , response , accessToken );
136+ sendUnauthorizedError ( response );
144137 break ;
145138 case INVALID :
146- // 유효하지 않은 액세스 토큰 - 리프레시 토큰 확인
139+ // 유효하지 않은 액세스 토큰 - 401 반환
147140 log .warn (LOG_INVALID_TOKEN );
148- handleTokenRefresh ( request , response , null );
141+ sendUnauthorizedError ( response );
149142 break ;
150143 }
151144 }
152145
146+ /**
147+ * 401 Unauthorized 응답을 반환합니다.
148+ */
149+ private void sendUnauthorizedError (HttpServletResponse response ) throws IOException {
150+ response .setStatus (HttpServletResponse .SC_UNAUTHORIZED );
151+ response .setContentType ("application/json" );
152+ response .setCharacterEncoding ("UTF-8" );
153+ response .getWriter ().write ("{\" error\" :\" Unauthorized\" ,\" message\" :\" 토큰이 만료되었거나 유효하지 않습니다. /api/auth/refresh를 호출하여 토큰을 재발급 받으세요.\" }" );
154+ }
155+
153156 /**
154157 * JWT 토큰에서 사용자 정보를 추출하여 Spring Security 인증 객체를 설정합니다.
155158 * @param token JWT 액세스 토큰
@@ -198,102 +201,4 @@ private String buildAuthority(String role) {
198201 return ROLE_PREFIX + (role != null ? role : DEFAULT_ROLE );
199202 }
200203
201- /**
202- * 리프레시 토큰을 사용하여 액세스 토큰을 갱신합니다.
203- * RTR(Refresh Token Rotation) 패턴을 적용하여 새로운 토큰 쌍을 생성합니다.
204- */
205- private void handleTokenRefresh (HttpServletRequest request , HttpServletResponse response , String expiredAccessToken ) {
206- try {
207- String refreshToken = cookieUtil .getRefreshTokenFromCookies (request );
208- if (refreshToken == null ) {
209- log .info (LOG_NO_REFRESH_TOKEN );
210- clearAuthenticationAndCookies (response );
211- return ;
212- }
213-
214- String loginId = extractLoginId (expiredAccessToken , refreshToken );
215- if (loginId == null ) {
216- log .warn (LOG_LOGIN_ID_EXTRACTION_FAILED );
217- clearAuthenticationAndCookies (response );
218- return ;
219- }
220-
221- if (!tokenProvider .validateRefreshToken (loginId , refreshToken )) {
222- log .info (LOG_INVALID_REFRESH_TOKEN , loginId );
223- clearAuthenticationAndCookies (response );
224- return ;
225- }
226-
227- MemberAdapter member = findMemberByLoginId (loginId );
228- if (member == null ) {
229- log .warn (LOG_MEMBER_NOT_FOUND , loginId );
230- clearAuthenticationAndCookies (response );
231- return ;
232- }
233-
234- refreshTokensAndSetAuthentication (response , loginId , member );
235- log .info (LOG_TOKEN_REFRESH_SUCCESS , loginId );
236-
237- } catch (Exception e ) {
238- log .error (LOG_TOKEN_REFRESH_FAILED , e .getMessage (), e );
239- clearAuthenticationAndCookies (response );
240- }
241- }
242-
243- /**
244- * loginId를 추출합니다 (만료된 토큰 또는 리프레시 토큰에서).
245- */
246- private String extractLoginId (String expiredAccessToken , String refreshToken ) {
247- String loginId = null ;
248- if (expiredAccessToken != null ) {
249- loginId = tokenProvider .getLoginIdFromExpiredToken (expiredAccessToken );
250- }
251- if (loginId == null ) {
252- loginId = tokenProvider .findUsernameByRefreshToken (refreshToken );
253- }
254- return loginId ;
255- }
256-
257- /**
258- * loginId로 회원 정보를 조회합니다 (Member 또는 OAuth2Member).
259- */
260- private MemberAdapter findMemberByLoginId (String loginId ) {
261- MemberAdapter member = memberRepository .findByLoginId (loginId ).orElse (null );
262-
263- if (member == null && oauth2MemberRepository != null ) {
264- member = oauth2MemberRepository .findByLoginId (loginId ).orElse (null );
265- }
266-
267- return member ;
268- }
269-
270- /**
271- * RTR 패턴으로 토큰을 갱신하고 인증을 설정합니다.
272- */
273- private void refreshTokensAndSetAuthentication (HttpServletResponse response , String loginId , MemberAdapter member ) {
274- // RTR(Refresh Token Rotation) 패턴: 기존 모든 토큰 삭제
275- tokenProvider .deleteAllTokens (loginId );
276-
277- // 새로운 액세스 토큰과 리프레시 토큰 생성
278- String newAccessToken = tokenProvider .generateAccessToken (member );
279- String newRefreshToken = tokenProvider .generateRefreshToken (member );
280-
281- // 새로운 토큰들을 쿠키에 설정
282- cookieUtil .setTokenCookies (response , newAccessToken , newRefreshToken );
283-
284- // 새로운 액세스 토큰으로 인증 설정
285- setAuthentication (newAccessToken );
286- }
287-
288- /**
289- * 인증 정보와 쿠키를 모두 클리어합니다.
290- */
291- private void clearAuthenticationAndCookies (HttpServletResponse response ) {
292- // Spring Security 인증 정보 클리어
293- SecurityContextHolder .clearContext ();
294-
295- // 쿠키 클리어
296- cookieUtil .clearTokenCookies (response );
297- }
298-
299204}
0 commit comments