diff --git a/src/main/java/com/backend/allreva/auth/application/AuthService.java b/src/main/java/com/backend/allreva/auth/application/AuthService.java index 7ac879e7..26ec3326 100644 --- a/src/main/java/com/backend/allreva/auth/application/AuthService.java +++ b/src/main/java/com/backend/allreva/auth/application/AuthService.java @@ -110,4 +110,18 @@ public UserInfoResponse reissueAccessToken(final String refreshToken) { .profileImageUrl(member.getMemberInfo().getProfileImageUrl()) .build(); } + + /** + * Redis에 저장된 Refresh Token을 제거합니다. + */ + public void logout(final String refreshToken) { + if (refreshToken == null) { + throw new TokenEmptyException(); + } + + // redis refresh token은 따로 만료기간이 없어서 영구 저장됨 -> 없다면 문제 있는거. + jwtService.validRefreshTokenExistInRedis(refreshToken); + + jwtService.deleteRefreshTokenInRedis(refreshToken); + } } diff --git a/src/main/java/com/backend/allreva/auth/application/CookieService.java b/src/main/java/com/backend/allreva/auth/application/CookieService.java index e7f763c7..4243867b 100644 --- a/src/main/java/com/backend/allreva/auth/application/CookieService.java +++ b/src/main/java/com/backend/allreva/auth/application/CookieService.java @@ -2,6 +2,8 @@ import com.backend.allreva.common.util.CookieUtils; import jakarta.servlet.http.HttpServletResponse; +import java.net.MalformedURLException; +import java.net.URL; import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Service; @@ -27,7 +29,24 @@ public void addRefreshTokenCookie( ); } + public void deleteRefreshTokenCookie( + final HttpServletResponse response, + final String domainName + ) { + CookieUtils.deleteCookie( + response, + isLocalhost(domainName) ? null : prodDomainName, + "refreshToken" + ); + } + private static boolean isLocalhost(String domain) { - return domain.contains("localhost"); + try { + URL url = new URL(domain); + String host = url.getHost(); + return host.contains("localhost") || host.contains("127.0.0.1"); + } catch (MalformedURLException e) { + return false; + } } } diff --git a/src/main/java/com/backend/allreva/auth/application/JwtService.java b/src/main/java/com/backend/allreva/auth/application/JwtService.java index 604c821a..1df407e2 100644 --- a/src/main/java/com/backend/allreva/auth/application/JwtService.java +++ b/src/main/java/com/backend/allreva/auth/application/JwtService.java @@ -165,4 +165,13 @@ public void validRefreshTokenExistInRedis(final String refreshToken) { throw new TokenNotMatchException(); } } + + /** + * Refresh Token을 Redis에서 삭제합니다. + * @param refreshToken Cookie에 저장되있던 Refresh Token + */ + public void deleteRefreshTokenInRedis(final String refreshToken) { + Optional refreshTokenFromRedis = refreshTokenRepository.findRefreshTokenByToken(refreshToken); + refreshTokenFromRedis.ifPresent(refreshTokenRepository::delete); + } } diff --git a/src/main/java/com/backend/allreva/auth/ui/AuthController.java b/src/main/java/com/backend/allreva/auth/ui/AuthController.java index b1b02300..c24533b6 100644 --- a/src/main/java/com/backend/allreva/auth/ui/AuthController.java +++ b/src/main/java/com/backend/allreva/auth/ui/AuthController.java @@ -62,4 +62,16 @@ public Response loginCheck( response.addHeader("Authorization", "Bearer " + userInfoResponse.accessToken()); return Response.onSuccess(userInfoResponse); } + + @GetMapping("/logout") + public Response logout( + @CookieValue(name = "refreshToken", required = false) final String refreshToken, + final HttpServletRequest request, + final HttpServletResponse response + ) { + String domainName = DomainUtils.getDomainName(request); + authService.logout(refreshToken); + cookieService.deleteRefreshTokenCookie(response, domainName); + return Response.onSuccess(); + } } diff --git a/src/main/java/com/backend/allreva/auth/ui/AuthControllerSwagger.java b/src/main/java/com/backend/allreva/auth/ui/AuthControllerSwagger.java index 2f7d78c4..ae68ede2 100644 --- a/src/main/java/com/backend/allreva/auth/ui/AuthControllerSwagger.java +++ b/src/main/java/com/backend/allreva/auth/ui/AuthControllerSwagger.java @@ -31,4 +31,11 @@ Response loginCheck( HttpServletRequest request, HttpServletResponse response ); + + @Operation(summary = "로그아웃", description = "refresh token을 삭제함으로써 로그아웃합니다.") + Response logout( + final String refreshToken, + final HttpServletRequest request, + final HttpServletResponse response + ); } diff --git a/src/main/java/com/backend/allreva/common/util/CookieUtils.java b/src/main/java/com/backend/allreva/common/util/CookieUtils.java index ccbe7302..a8794d5a 100644 --- a/src/main/java/com/backend/allreva/common/util/CookieUtils.java +++ b/src/main/java/com/backend/allreva/common/util/CookieUtils.java @@ -28,4 +28,22 @@ public static void addCookie( response.addHeader(HttpHeaders.SET_COOKIE, cookie.toString()); } + + // 쿠키 제거 + public static void deleteCookie( + final HttpServletResponse response, + final String cookieDomain, + final String name + ) { + ResponseCookie cookie = ResponseCookie.from(name, "") + .domain(cookieDomain) + .path("/") + .maxAge(0) // maxAge 0 + .httpOnly(true) + .secure(true) + .sameSite("None") + .build(); + + response.addHeader(HttpHeaders.SET_COOKIE, cookie.toString()); + } } diff --git a/src/main/java/com/backend/allreva/common/util/DomainUtils.java b/src/main/java/com/backend/allreva/common/util/DomainUtils.java index cd89d0b4..58615fe1 100644 --- a/src/main/java/com/backend/allreva/common/util/DomainUtils.java +++ b/src/main/java/com/backend/allreva/common/util/DomainUtils.java @@ -11,7 +11,10 @@ public final class DomainUtils { public static String getDomainName(HttpServletRequest request) { // ️Nginx 프록시를 고려하여 `Origin` 헤더 확인 String domainName = request.getHeader("Origin"); - log.info("Origin: {}", domainName); + if (domainName == null) { + domainName = request.getHeader("Referer"); + } + log.info("DomainName: {}", domainName); return domainName; }