From 4a3bd6486b4826966bce830c08335b45c5777cf9 Mon Sep 17 00:00:00 2001 From: nayonsoso Date: Sat, 10 May 2025 02:35:57 +0900 Subject: [PATCH 1/4] =?UTF-8?q?feat:=20=EC=95=A1=EC=84=B8=EC=8A=A4=20?= =?UTF-8?q?=ED=86=A0=ED=81=B0=EC=97=90=20=ED=95=B4=EB=8B=B9=ED=95=98?= =?UTF-8?q?=EB=8A=94=20=EB=A6=AC=ED=94=84=EB=A0=88=EC=8B=9C=20=ED=86=A0?= =?UTF-8?q?=ED=81=B0=20=EC=82=AD=EC=A0=9C=20=ED=95=A8=EC=88=98=20=EA=B5=AC?= =?UTF-8?q?=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../auth/service/AuthTokenProvider.java | 6 ++++++ .../auth/service/AuthTokenProviderTest.java | 14 ++++++++++++++ 2 files changed, 20 insertions(+) diff --git a/src/main/java/com/example/solidconnection/auth/service/AuthTokenProvider.java b/src/main/java/com/example/solidconnection/auth/service/AuthTokenProvider.java index d44f3bc8f..2e43e8be9 100644 --- a/src/main/java/com/example/solidconnection/auth/service/AuthTokenProvider.java +++ b/src/main/java/com/example/solidconnection/auth/service/AuthTokenProvider.java @@ -49,6 +49,12 @@ public boolean isValidRefreshToken(String requestedRefreshToken) { return Objects.equals(requestedRefreshToken, foundRefreshToken); } + public void deleteRefreshTokenByAccessToken(AccessToken accessToken) { + String subject = accessToken.subject().value(); + String refreshTokenKey = TokenType.REFRESH.addPrefix(subject); + redisTemplate.delete(refreshTokenKey); + } + @Override public boolean isTokenBlacklisted(String accessToken) { String blackListTokenKey = TokenType.BLACKLIST.addPrefix(accessToken); diff --git a/src/test/java/com/example/solidconnection/auth/service/AuthTokenProviderTest.java b/src/test/java/com/example/solidconnection/auth/service/AuthTokenProviderTest.java index 2a0bd9b73..dc35ab3a7 100644 --- a/src/test/java/com/example/solidconnection/auth/service/AuthTokenProviderTest.java +++ b/src/test/java/com/example/solidconnection/auth/service/AuthTokenProviderTest.java @@ -69,6 +69,20 @@ class 리프레시_토큰을_제공한다 { () -> assertThat(authTokenProvider.isValidRefreshToken(fakeRefreshToken.token())).isFalse() ); } + + @Test + void 액세서_토큰에_해당하는_리프레시_토큰을_삭제한다() { + // given + authTokenProvider.generateAndSaveRefreshToken(subject); + AccessToken accessToken = authTokenProvider.generateAccessToken(subject); + + // when + authTokenProvider.deleteRefreshTokenByAccessToken(accessToken); + + // then + String refreshTokenKey = TokenType.REFRESH.addPrefix(subject.value()); + assertThat(redisTemplate.opsForValue().get(refreshTokenKey)).isNull(); + } } @Nested From 1ae1060c4bfc97fd0a17bb5a2a290189dd3174f3 Mon Sep 17 00:00:00 2001 From: nayonsoso Date: Sat, 10 May 2025 02:39:06 +0900 Subject: [PATCH 2/4] =?UTF-8?q?refactor:=20=EB=A1=9C=EA=B7=B8=EC=95=84?= =?UTF-8?q?=EC=9B=83=20=EC=8B=9C,=20=EB=A6=AC=ED=94=84=EB=A0=88=EC=8B=9C?= =?UTF-8?q?=20=ED=86=A0=ED=81=B0=20=EC=82=AD=EC=A0=9C=ED=95=98=EB=8F=84?= =?UTF-8?q?=EB=A1=9D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../solidconnection/auth/service/AuthService.java | 1 + .../auth/service/AuthServiceTest.java | 15 +++++++++++++-- 2 files changed, 14 insertions(+), 2 deletions(-) diff --git a/src/main/java/com/example/solidconnection/auth/service/AuthService.java b/src/main/java/com/example/solidconnection/auth/service/AuthService.java index 86220d863..bce4073da 100644 --- a/src/main/java/com/example/solidconnection/auth/service/AuthService.java +++ b/src/main/java/com/example/solidconnection/auth/service/AuthService.java @@ -24,6 +24,7 @@ public class AuthService { * */ public void signOut(String token) { AccessToken accessToken = authTokenProvider.toAccessToken(token); + authTokenProvider.deleteRefreshTokenByAccessToken(accessToken); authTokenProvider.addToBlacklist(accessToken); } diff --git a/src/test/java/com/example/solidconnection/auth/service/AuthServiceTest.java b/src/test/java/com/example/solidconnection/auth/service/AuthServiceTest.java index 759c90d88..b1fe5bc09 100644 --- a/src/test/java/com/example/solidconnection/auth/service/AuthServiceTest.java +++ b/src/test/java/com/example/solidconnection/auth/service/AuthServiceTest.java @@ -1,5 +1,6 @@ package com.example.solidconnection.auth.service; +import com.example.solidconnection.auth.domain.TokenType; import com.example.solidconnection.auth.dto.ReissueRequest; import com.example.solidconnection.auth.dto.ReissueResponse; import com.example.solidconnection.custom.exception.CustomException; @@ -12,12 +13,14 @@ import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.redis.core.RedisTemplate; import java.time.LocalDate; import static com.example.solidconnection.custom.exception.ErrorCode.REFRESH_TOKEN_EXPIRED; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatCode; +import static org.junit.jupiter.api.Assertions.assertAll; @DisplayName("인증 서비스 테스트") @TestContainerSpringBootTest @@ -32,16 +35,24 @@ class AuthServiceTest { @Autowired private SiteUserRepository siteUserRepository; + @Autowired + private RedisTemplate redisTemplate; + @Test void 로그아웃한다() { // given - AccessToken accessToken = authTokenProvider.generateAccessToken(new Subject("subject")); // todo: #296 + Subject subject = new Subject("subject"); + AccessToken accessToken = authTokenProvider.generateAccessToken(subject); // todo: #296 // when authService.signOut(accessToken.token()); // then - assertThat(authTokenProvider.isTokenBlacklisted(accessToken.token())).isTrue(); + String refreshTokenKey = TokenType.REFRESH.addPrefix(subject.value()); + assertAll( + () -> assertThat(redisTemplate.opsForValue().get(refreshTokenKey)).isNull(), + () -> assertThat(authTokenProvider.isTokenBlacklisted(accessToken.token())).isTrue() + ); } @Test From a1e2993870f616e477d0c5913e9e94b27a5795bd Mon Sep 17 00:00:00 2001 From: nayonsoso Date: Sat, 10 May 2025 02:44:21 +0900 Subject: [PATCH 3/4] =?UTF-8?q?refactor:=20=ED=83=88=ED=87=B4=20=EC=8B=9C,?= =?UTF-8?q?=20=EB=A1=9C=EA=B7=B8=EC=95=84=EC=9B=83=ED=95=98=EB=8F=84?= =?UTF-8?q?=EB=A1=9D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../auth/controller/AuthController.java | 9 +++++++-- .../solidconnection/auth/service/AuthService.java | 7 +++++-- .../solidconnection/auth/service/AuthServiceTest.java | 11 +++++++++-- 3 files changed, 21 insertions(+), 6 deletions(-) diff --git a/src/main/java/com/example/solidconnection/auth/controller/AuthController.java b/src/main/java/com/example/solidconnection/auth/controller/AuthController.java index c4b9127d5..312ad21af 100644 --- a/src/main/java/com/example/solidconnection/auth/controller/AuthController.java +++ b/src/main/java/com/example/solidconnection/auth/controller/AuthController.java @@ -107,9 +107,14 @@ public ResponseEntity signOut( @PatchMapping("/quit") public ResponseEntity quit( - @AuthorizedUser SiteUser siteUser + @AuthorizedUser SiteUser siteUser, + Authentication authentication ) { - authService.quit(siteUser); + String accessToken = (String) authentication.getCredentials(); + if (accessToken == null || accessToken.isBlank()) { + throw new CustomException(ErrorCode.AUTHENTICATION_FAILED, "토큰이 없습니다."); + } + authService.quit(siteUser, accessToken); return ResponseEntity.ok().build(); } diff --git a/src/main/java/com/example/solidconnection/auth/service/AuthService.java b/src/main/java/com/example/solidconnection/auth/service/AuthService.java index bce4073da..50926ca93 100644 --- a/src/main/java/com/example/solidconnection/auth/service/AuthService.java +++ b/src/main/java/com/example/solidconnection/auth/service/AuthService.java @@ -19,8 +19,9 @@ public class AuthService { private final AuthTokenProvider authTokenProvider; /* - * 로그아웃 한다. + * 로그아웃한다. * - 엑세스 토큰을 블랙리스트에 추가한다. + * - 리프레시 토큰을 삭제한다. * */ public void signOut(String token) { AccessToken accessToken = authTokenProvider.toAccessToken(token); @@ -32,11 +33,13 @@ public void signOut(String token) { * 탈퇴한다. * - 탈퇴한 시점의 다음날을 탈퇴일로 잡는다. * - e.g. 2024-01-01 18:00 탈퇴 시, 2024-01-02 00:00 가 탈퇴일이 된다. + * - 로그아웃한다. * */ @Transactional - public void quit(SiteUser siteUser) { + public void quit(SiteUser siteUser, String token) { // todo: #299를 작업하며 인자를 (String token)만 받도록 수정해야 함 LocalDate tomorrow = LocalDate.now().plusDays(1); siteUser.setQuitedAt(tomorrow); + signOut(token); } /* diff --git a/src/test/java/com/example/solidconnection/auth/service/AuthServiceTest.java b/src/test/java/com/example/solidconnection/auth/service/AuthServiceTest.java index b1fe5bc09..18c7c4ef0 100644 --- a/src/test/java/com/example/solidconnection/auth/service/AuthServiceTest.java +++ b/src/test/java/com/example/solidconnection/auth/service/AuthServiceTest.java @@ -59,13 +59,20 @@ class AuthServiceTest { void 탈퇴한다() { // given SiteUser siteUser = createSiteUser(); + Subject subject = authTokenProvider.toSubject(siteUser); + AccessToken accessToken = authTokenProvider.generateAccessToken(subject); // todo: #296 // when - authService.quit(siteUser); + authService.quit(siteUser, accessToken.token()); // then LocalDate tomorrow = LocalDate.now().plusDays(1); - assertThat(siteUser.getQuitedAt()).isEqualTo(tomorrow); + String refreshTokenKey = TokenType.REFRESH.addPrefix(subject.value()); + assertAll( + () -> assertThat(siteUser.getQuitedAt()).isEqualTo(tomorrow), + () -> assertThat(redisTemplate.opsForValue().get(refreshTokenKey)).isNull(), + () -> assertThat(authTokenProvider.isTokenBlacklisted(accessToken.token())).isTrue() + ); } @Nested From 8deddfff1163fae26c02d826ad86ccea7ed22d38 Mon Sep 17 00:00:00 2001 From: nayonsoso Date: Sat, 10 May 2025 02:45:11 +0900 Subject: [PATCH 4/4] =?UTF-8?q?refactor:=20=EC=97=91=EC=84=B8=EC=8A=A4=20?= =?UTF-8?q?=ED=86=A0=ED=81=B0=20=EA=B0=80=EC=A0=B8=EC=98=A4=EB=8A=94=20?= =?UTF-8?q?=ED=95=A8=EC=88=98=20=EB=B6=84=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Authentication 자체가 null 인 경우도 예외처리 --- .../auth/controller/AuthController.java | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/src/main/java/com/example/solidconnection/auth/controller/AuthController.java b/src/main/java/com/example/solidconnection/auth/controller/AuthController.java index 312ad21af..0df89426e 100644 --- a/src/main/java/com/example/solidconnection/auth/controller/AuthController.java +++ b/src/main/java/com/example/solidconnection/auth/controller/AuthController.java @@ -97,10 +97,7 @@ public ResponseEntity signUp( public ResponseEntity signOut( Authentication authentication ) { - String accessToken = (String) authentication.getCredentials(); - if (accessToken == null || accessToken.isBlank()) { - throw new CustomException(ErrorCode.AUTHENTICATION_FAILED, "토큰이 없습니다."); - } + String accessToken = getAccessToken(authentication); authService.signOut(accessToken); return ResponseEntity.ok().build(); } @@ -108,12 +105,9 @@ public ResponseEntity signOut( @PatchMapping("/quit") public ResponseEntity quit( @AuthorizedUser SiteUser siteUser, - Authentication authentication + Authentication authentication // todo: #299를 작업하며 인자를 (Authentication authentication)만 받도록 수정해야 함 ) { - String accessToken = (String) authentication.getCredentials(); - if (accessToken == null || accessToken.isBlank()) { - throw new CustomException(ErrorCode.AUTHENTICATION_FAILED, "토큰이 없습니다."); - } + String accessToken = getAccessToken(authentication); authService.quit(siteUser, accessToken); return ResponseEntity.ok().build(); } @@ -125,4 +119,11 @@ public ResponseEntity reissueToken( ReissueResponse reissueResponse = authService.reissue(reissueRequest); return ResponseEntity.ok(reissueResponse); } + + private String getAccessToken(Authentication authentication) { + if (authentication == null || !(authentication.getCredentials() instanceof String accessToken)) { + throw new CustomException(ErrorCode.AUTHENTICATION_FAILED, "엑세스 토큰이 없습니다."); + } + return accessToken; + } }