diff --git a/src/main/java/com/kcy/fitapet/domain/member/api/MemberApi.java b/src/main/java/com/kcy/fitapet/domain/member/api/MemberApi.java index b326b091..f98f9310 100644 --- a/src/main/java/com/kcy/fitapet/domain/member/api/MemberApi.java +++ b/src/main/java/com/kcy/fitapet/domain/member/api/MemberApi.java @@ -130,15 +130,19 @@ public ResponseEntity signOut( @AccessTokenInfo AccessToken accessToken, @CookieValue(value = "refreshToken", required = false) @Valid String refreshToken, HttpServletRequest request, HttpServletResponse response) { + if (accessToken.isReissued()) { + refreshToken = response.getHeader(HttpHeaders.SET_COOKIE).substring(AuthConstants.REFRESH_TOKEN.getValue().length() + 1); + log.info("reissued refresh token: {}", refreshToken); + } + memberAuthService.logout(accessToken, refreshToken); - ResponseCookie cookie; - if (refreshToken != null) - cookie = cookieUtil.deleteCookie(request, response, REFRESH_TOKEN.getValue()) - .orElseThrow(() -> new AuthErrorException(AuthErrorCode.REFRESH_TOKEN_NOT_FOUND, "존재하지 않는 쿠키입니다.")); - else - cookie = cookieUtil.createCookie(REFRESH_TOKEN.getValue(), "", 0); + if (!StringUtils.hasText(refreshToken)) { + return ResponseEntity.ok(SuccessResponse.noContent()); + } + ResponseCookie cookie = cookieUtil.deleteCookie(request, response, REFRESH_TOKEN.getValue()) + .orElseThrow(() -> new AuthErrorException(AuthErrorCode.REFRESH_TOKEN_NOT_FOUND, "존재하지 않는 쿠키입니다.")); return ResponseEntity.ok().header(HttpHeaders.SET_COOKIE, cookie.toString()).body(SuccessResponse.noContent()); } @@ -151,11 +155,7 @@ public ResponseEntity signOut( }) @GetMapping("/refresh") public ResponseEntity refresh(@CookieValue("refreshToken") @Valid String refreshToken) { - if (refreshToken == null) { - throw new AuthErrorException(AuthErrorCode.REFRESH_TOKEN_NOT_FOUND, "존재하지 않는 쿠키입니다."); - } Map tokens = memberAuthService.refresh(refreshToken); - return getResponseEntity(tokens); } diff --git a/src/main/java/com/kcy/fitapet/domain/member/service/component/MemberAuthService.java b/src/main/java/com/kcy/fitapet/domain/member/service/component/MemberAuthService.java index 46514de5..33176851 100644 --- a/src/main/java/com/kcy/fitapet/domain/member/service/component/MemberAuthService.java +++ b/src/main/java/com/kcy/fitapet/domain/member/service/component/MemberAuthService.java @@ -79,10 +79,11 @@ public Map login(SignInReq dto) { } @Transactional - public void logout(AccessToken accessToken, String requestRefreshToken) { - if (requestRefreshToken != null) + public void logout(AccessToken requestAccessToken, String requestRefreshToken) { + forbiddenTokenService.register(requestAccessToken); + + if (!StringUtils.hasText(requestRefreshToken)) refreshTokenService.logout(requestRefreshToken); - forbiddenTokenService.register(accessToken); } @Transactional diff --git a/src/main/java/com/kcy/fitapet/global/common/resolver/access/AccessToken.java b/src/main/java/com/kcy/fitapet/global/common/resolver/access/AccessToken.java index 2e6ed742..f3197f9a 100644 --- a/src/main/java/com/kcy/fitapet/global/common/resolver/access/AccessToken.java +++ b/src/main/java/com/kcy/fitapet/global/common/resolver/access/AccessToken.java @@ -6,6 +6,10 @@ public record AccessToken( String accessToken, Long userId, - LocalDateTime expiryDate + LocalDateTime expiryDate, + boolean isReissued ) { + public static AccessToken of(String accessToken, Long userId, LocalDateTime expiryDate, boolean isReissued) { + return new AccessToken(accessToken, userId, expiryDate, isReissued); + } } diff --git a/src/main/java/com/kcy/fitapet/global/common/resolver/access/AccessTokenInfoResolver.java b/src/main/java/com/kcy/fitapet/global/common/resolver/access/AccessTokenInfoResolver.java index a5341202..097f01ef 100644 --- a/src/main/java/com/kcy/fitapet/global/common/resolver/access/AccessTokenInfoResolver.java +++ b/src/main/java/com/kcy/fitapet/global/common/resolver/access/AccessTokenInfoResolver.java @@ -1,10 +1,13 @@ package com.kcy.fitapet.global.common.resolver.access; +import com.kcy.fitapet.global.common.util.cookie.CookieUtil; import com.kcy.fitapet.global.common.util.jwt.AuthConstants; import com.kcy.fitapet.global.common.util.jwt.JwtUtil; import com.kcy.fitapet.global.common.util.jwt.exception.AuthErrorCode; import com.kcy.fitapet.global.common.util.jwt.exception.AuthErrorException; +import jakarta.servlet.http.Cookie; import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; import lombok.AccessLevel; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; @@ -23,6 +26,7 @@ @Component public class AccessTokenInfoResolver implements HandlerMethodArgumentResolver { private final JwtUtil jwtUtil; + private final CookieUtil cookieUtil; @Override public boolean supportsParameter(MethodParameter parameter) { @@ -37,17 +41,28 @@ public Object resolveArgument( NativeWebRequest webRequest, WebDataBinderFactory binderFactory) { final var httpServletRequest = (HttpServletRequest) webRequest.getNativeRequest(); - String accessToken = jwtUtil.resolveToken(httpServletRequest.getHeader(AuthConstants.AUTH_HEADER.getValue())); + final var httpServletResponse = (HttpServletResponse) webRequest.getNativeResponse(); + boolean isReissued = false; - if (!StringUtils.hasText(accessToken)) { - log.error("Access Token is empty"); - throw new AuthErrorException(AuthErrorCode.EMPTY_ACCESS_TOKEN, "access token is empty"); + String reissuedAccessToken = httpServletResponse.getHeader(AuthConstants.REISSUED_ACCESS_TOKEN.getValue()); + + String accessToken; + if (!StringUtils.hasText(reissuedAccessToken)) { + accessToken = jwtUtil.resolveToken(httpServletRequest.getHeader(AuthConstants.AUTH_HEADER.getValue())); + + if (!StringUtils.hasText(accessToken)) { + log.error("Access Token is empty"); + throw new AuthErrorException(AuthErrorCode.EMPTY_ACCESS_TOKEN, "access token is empty"); + } + } else { + accessToken = reissuedAccessToken; + isReissued = true; } Long userId = jwtUtil.getUserIdFromToken(accessToken); LocalDateTime expiryDate = jwtUtil.getExpiryDate(accessToken); + log.info("access token expiryDate : {}", expiryDate); - return new AccessToken(accessToken, userId, expiryDate); + return AccessToken.of(accessToken, userId, expiryDate, isReissued); } - } diff --git a/src/main/java/com/kcy/fitapet/global/common/response/handler/GlobalExceptionHandler.java b/src/main/java/com/kcy/fitapet/global/common/response/handler/GlobalExceptionHandler.java index cebc2413..04df35d3 100644 --- a/src/main/java/com/kcy/fitapet/global/common/response/handler/GlobalExceptionHandler.java +++ b/src/main/java/com/kcy/fitapet/global/common/response/handler/GlobalExceptionHandler.java @@ -48,6 +48,11 @@ protected ResponseEntity handleAuthErrorException(AuthErrorExcept return ResponseEntity.status(e.getErrorCode().getHttpStatus()).body(response); } + /** + * API 호출 시 인가 관련 예외를 처리하는 메서드 + * @param e AccessDeniedException + * @return ResponseEntity + */ @ExceptionHandler(AccessDeniedException.class) protected ResponseEntity handleAccessDeniedException(AccessDeniedException e) { log.warn("handleAccessDeniedException : {}", e.getMessage()); diff --git a/src/main/java/com/kcy/fitapet/global/common/security/authorization/PreAuthorizeService.java b/src/main/java/com/kcy/fitapet/global/common/security/authorization/PreAuthorizeService.java index eb82ed9c..864e1285 100644 --- a/src/main/java/com/kcy/fitapet/global/common/security/authorization/PreAuthorizeService.java +++ b/src/main/java/com/kcy/fitapet/global/common/security/authorization/PreAuthorizeService.java @@ -14,8 +14,7 @@ public class PreAuthorizeService { public boolean test(Long requestMemberId) { log.info("is start pre authorization check"); -// authentication = SecurityContextHolder.getContext().getAuthentication(); - log.info("user role type : {}",requestMemberId); + log.info("user role type : {}", requestMemberId); return true; } } diff --git a/src/main/java/com/kcy/fitapet/global/common/security/filter/JwtAuthenticationFilter.java b/src/main/java/com/kcy/fitapet/global/common/security/filter/JwtAuthenticationFilter.java index 87c272cc..c843b583 100644 --- a/src/main/java/com/kcy/fitapet/global/common/security/filter/JwtAuthenticationFilter.java +++ b/src/main/java/com/kcy/fitapet/global/common/security/filter/JwtAuthenticationFilter.java @@ -102,7 +102,7 @@ private String resolveAccessToken(HttpServletRequest request, HttpServletRespons } private String reissueAccessToken(HttpServletRequest request, HttpServletResponse response) { - Cookie refreshTokenCookie = cookieUtil.getCookie(request, REFRESH_TOKEN.getValue()) + Cookie refreshTokenCookie = cookieUtil.getCookieFromRequest(request, REFRESH_TOKEN.getValue()) .orElseThrow(() -> new AuthErrorException(AuthErrorCode.REFRESH_TOKEN_NOT_FOUND, "Refresh token not found")); String requestRefreshToken = refreshTokenCookie.getValue(); diff --git a/src/main/java/com/kcy/fitapet/global/common/util/cookie/CookieUtil.java b/src/main/java/com/kcy/fitapet/global/common/util/cookie/CookieUtil.java index ac6d09d2..21d59425 100644 --- a/src/main/java/com/kcy/fitapet/global/common/util/cookie/CookieUtil.java +++ b/src/main/java/com/kcy/fitapet/global/common/util/cookie/CookieUtil.java @@ -3,6 +3,7 @@ import jakarta.servlet.http.Cookie; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; +import org.springframework.http.HttpHeaders; import org.springframework.http.ResponseCookie; import org.springframework.stereotype.Component; @@ -17,7 +18,7 @@ public class CookieUtil { * @param cookieName String : 찾을 쿠키의 이름 * @return Optional : 쿠키가 존재하면 해당 쿠키를, 존재하지 않으면 Optional.empty()를 반환합니다. */ - public Optional getCookie(HttpServletRequest request, String cookieName) { + public Optional getCookieFromRequest(HttpServletRequest request, String cookieName) { Cookie[] cookies = request.getCookies(); if (cookies == null) { return Optional.empty(); diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index 2273becd..84029370 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -48,7 +48,7 @@ jwt: secret: ${JWT_SECRET} token: # milliseconds 단위 - access-expiration-time: 1800000 # 30m (30 * 60 * 1000) + access-expiration-time: 2000 # 30m (30 * 60 * 1000) refresh-expiration-time: 604800000 # 7d (7 * 24 * 60 * 60 * 1000) sms-auth-expiration-time: 180000 # 3m (3 * 60 * 1000)