From b23767c049fc14adad2a2b3aeb25ce86449aa14b Mon Sep 17 00:00:00 2001 From: nayonsoso Date: Tue, 29 Apr 2025 14:14:35 +0900 Subject: [PATCH 1/7] =?UTF-8?q?refactor:=20=ED=95=A8=EC=88=98=20=EB=B6=84?= =?UTF-8?q?=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - subject의 기준이 SiteUser의 id가 아니게 바뀌더라도 변경 부분이 최소화되게 한다. - SiteUser의 subject를 사용하는 곳은 accessToken과 refreshToken를 관리하는 AuthTokenProvider 의 영역이므로 이 위치에 함수를 생성한다. --- .../solidconnection/auth/service/AuthTokenProvider.java | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) 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 da040a8d5..fbc0d6470 100644 --- a/src/main/java/com/example/solidconnection/auth/service/AuthTokenProvider.java +++ b/src/main/java/com/example/solidconnection/auth/service/AuthTokenProvider.java @@ -18,7 +18,7 @@ public AuthTokenProvider(JwtProperties jwtProperties, RedisTemplate findBlackListToken(String subject) { public String getEmail(String token) { return parseSubjectIgnoringExpiration(token, jwtProperties.secret()); } + + private String getSubject(SiteUser siteUser) { + return siteUser.getId().toString(); + } } From c9b545f1078458f172904707fd8ece9ea1ad4545 Mon Sep 17 00:00:00 2001 From: nayonsoso Date: Tue, 29 Apr 2025 14:15:47 +0900 Subject: [PATCH 2/7] =?UTF-8?q?chore:=20=EC=82=AC=EC=9A=A9=ED=95=98?= =?UTF-8?q?=EC=A7=80=20=EC=95=8A=EB=8A=94=20=ED=95=A8=EC=88=98=20=EC=82=AD?= =?UTF-8?q?=EC=A0=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../solidconnection/auth/service/AuthTokenProvider.java | 6 ------ 1 file changed, 6 deletions(-) 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 fbc0d6470..b682a4b39 100644 --- a/src/main/java/com/example/solidconnection/auth/service/AuthTokenProvider.java +++ b/src/main/java/com/example/solidconnection/auth/service/AuthTokenProvider.java @@ -8,8 +8,6 @@ import java.util.Optional; -import static com.example.solidconnection.util.JwtUtils.parseSubjectIgnoringExpiration; - @Component public class AuthTokenProvider extends TokenProvider { @@ -47,10 +45,6 @@ public Optional findBlackListToken(String subject) { return Optional.ofNullable(redisTemplate.opsForValue().get(blackListTokenKey)); } - public String getEmail(String token) { - return parseSubjectIgnoringExpiration(token, jwtProperties.secret()); - } - private String getSubject(SiteUser siteUser) { return siteUser.getId().toString(); } From abb3026f683a69968dc4d15f1322fc97f58f3d4a Mon Sep 17 00:00:00 2001 From: nayonsoso Date: Tue, 29 Apr 2025 14:40:59 +0900 Subject: [PATCH 3/7] =?UTF-8?q?refactor:=20=EA=B0=80=EB=8F=85=EC=84=B1?= =?UTF-8?q?=EC=9D=84=20=EC=9C=84=ED=95=B4=20=ED=95=A8=EC=88=98=20=EC=9C=84?= =?UTF-8?q?=EC=B9=98=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - parseSubject 가 parseSubjectIgnoringExpiration 보다 먼저 읽히도록 --- .../java/com/example/solidconnection/util/JwtUtils.java | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/main/java/com/example/solidconnection/util/JwtUtils.java b/src/main/java/com/example/solidconnection/util/JwtUtils.java index d3ea8fed9..d295f2a3e 100644 --- a/src/main/java/com/example/solidconnection/util/JwtUtils.java +++ b/src/main/java/com/example/solidconnection/util/JwtUtils.java @@ -28,19 +28,19 @@ public static String parseTokenFromRequest(HttpServletRequest request) { return token.substring(TOKEN_PREFIX.length()); } - public static String parseSubjectIgnoringExpiration(String token, String secretKey) { + public static String parseSubject(String token, String secretKey) { try { return parseClaims(token, secretKey).getSubject(); - } catch (ExpiredJwtException e) { - return e.getClaims().getSubject(); } catch (Exception e) { throw new CustomException(INVALID_TOKEN); } } - public static String parseSubject(String token, String secretKey) { + public static String parseSubjectIgnoringExpiration(String token, String secretKey) { try { return parseClaims(token, secretKey).getSubject(); + } catch (ExpiredJwtException e) { + return e.getClaims().getSubject(); } catch (Exception e) { throw new CustomException(INVALID_TOKEN); } From 71a7689d6eeefc8b149c8fe8fda4ec7bae91e15b Mon Sep 17 00:00:00 2001 From: nayonsoso Date: Wed, 30 Apr 2025 12:59:28 +0900 Subject: [PATCH 4/7] =?UTF-8?q?refactor:=20=EC=9E=AC=EB=B0=9C=EA=B8=89=20?= =?UTF-8?q?=EB=A1=9C=EC=A7=81=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - subject가 아니라 엑세스 토큰으로 리프레시 토큰을 찾도록 --- .../solidconnection/auth/controller/AuthController.java | 6 +++--- .../example/solidconnection/auth/service/AuthService.java | 8 ++++++-- 2 files changed, 9 insertions(+), 5 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 9c84e8d22..cf78ec2d2 100644 --- a/src/main/java/com/example/solidconnection/auth/controller/AuthController.java +++ b/src/main/java/com/example/solidconnection/auth/controller/AuthController.java @@ -116,11 +116,11 @@ public ResponseEntity quit( public ResponseEntity reissueToken( Authentication authentication ) { - String token = authentication.getCredentials().toString(); - if (token == null) { + String accessToken = authentication.getCredentials().toString(); + if (accessToken == null) { throw new CustomException(ErrorCode.AUTHENTICATION_FAILED, "토큰이 없습니다."); } - ReissueResponse reissueResponse = authService.reissue(token); + ReissueResponse reissueResponse = authService.reissue(accessToken); return ResponseEntity.ok(reissueResponse); } } 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 04bcadde7..486e01769 100644 --- a/src/main/java/com/example/solidconnection/auth/service/AuthService.java +++ b/src/main/java/com/example/solidconnection/auth/service/AuthService.java @@ -2,6 +2,7 @@ import com.example.solidconnection.auth.dto.ReissueResponse; +import com.example.solidconnection.config.security.JwtProperties; import com.example.solidconnection.custom.exception.CustomException; import com.example.solidconnection.siteuser.domain.SiteUser; import lombok.RequiredArgsConstructor; @@ -12,12 +13,14 @@ import java.util.Optional; import static com.example.solidconnection.custom.exception.ErrorCode.REFRESH_TOKEN_EXPIRED; +import static com.example.solidconnection.util.JwtUtils.parseSubject; @RequiredArgsConstructor @Service public class AuthService { private final AuthTokenProvider authTokenProvider; + private final JwtProperties jwtProperties; /* * 로그아웃 한다. @@ -43,14 +46,15 @@ public void quit(SiteUser siteUser) { * - 리프레시 토큰이 만료되었거나, 존재하지 않는다면 예외 응답을 반환한다. * - 리프레시 토큰이 존재한다면, 액세스 토큰을 재발급한다. * */ - public ReissueResponse reissue(String subject) { + public ReissueResponse reissue(String accessToken) { // 리프레시 토큰 만료 확인 + String subject = parseSubject(accessToken, jwtProperties.secret()); Optional optionalRefreshToken = authTokenProvider.findRefreshToken(subject); if (optionalRefreshToken.isEmpty()) { throw new CustomException(REFRESH_TOKEN_EXPIRED); } // 액세스 토큰 재발급 - String newAccessToken = authTokenProvider.generateAccessToken(subject); + String newAccessToken = authTokenProvider.generateAccessToken(accessToken); return new ReissueResponse(newAccessToken); } } From 9beb32c51274ea6c69c27a6649b4c524faa48624 Mon Sep 17 00:00:00 2001 From: nayonsoso Date: Fri, 2 May 2025 13:35:00 +0900 Subject: [PATCH 5/7] =?UTF-8?q?refactor:=20=EB=A6=AC=ED=94=84=EB=A0=88?= =?UTF-8?q?=EC=8B=9C=20=ED=86=A0=ED=81=B0=20=EA=B8=B0=EA=B0=84=20=EC=97=B0?= =?UTF-8?q?=EC=9E=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 참고: discussion #292 --- .../example/solidconnection/auth/domain/TokenType.java | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/main/java/com/example/solidconnection/auth/domain/TokenType.java b/src/main/java/com/example/solidconnection/auth/domain/TokenType.java index caf1c7a9d..560b0e139 100644 --- a/src/main/java/com/example/solidconnection/auth/domain/TokenType.java +++ b/src/main/java/com/example/solidconnection/auth/domain/TokenType.java @@ -5,16 +5,16 @@ @Getter public enum TokenType { - ACCESS("ACCESS:", 1000 * 60 * 60), // 1hour - REFRESH("REFRESH:", 1000 * 60 * 60 * 24 * 7), // 7days + ACCESS("ACCESS:", 1000L * 60 * 60), // 1hour + REFRESH("REFRESH:", 1000L * 60 * 60 * 24 * 90), // 90days BLACKLIST("BLACKLIST:", ACCESS.expireTime), - SIGN_UP("SIGN_UP:", 1000 * 60 * 10), // 10min + SIGN_UP("SIGN_UP:", 1000L * 60 * 10), // 10min ; private final String prefix; - private final int expireTime; + private final long expireTime; - TokenType(String prefix, int expireTime) { + TokenType(String prefix, long expireTime) { this.prefix = prefix; this.expireTime = expireTime; } From 699d9ab999d4d60ab22e12e5a5fe7839f080ab32 Mon Sep 17 00:00:00 2001 From: nayonsoso Date: Fri, 2 May 2025 14:47:30 +0900 Subject: [PATCH 6/7] =?UTF-8?q?refactor:=20=EC=95=A1=EC=84=B8=EC=8A=A4=20?= =?UTF-8?q?=ED=86=A0=ED=81=B0=20=EC=9E=AC=EB=B0=9C=EA=B8=89=20=EB=A1=9C?= =?UTF-8?q?=EC=A7=81=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../auth/controller/AuthController.java | 9 +++------ .../auth/dto/ReissueRequest.java | 8 ++++++++ .../auth/service/AuthService.java | 19 +++++++++++-------- 3 files changed, 22 insertions(+), 14 deletions(-) create mode 100644 src/main/java/com/example/solidconnection/auth/dto/ReissueRequest.java 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 cf78ec2d2..7fe543bd7 100644 --- a/src/main/java/com/example/solidconnection/auth/controller/AuthController.java +++ b/src/main/java/com/example/solidconnection/auth/controller/AuthController.java @@ -3,6 +3,7 @@ import com.example.solidconnection.auth.dto.EmailSignInRequest; import com.example.solidconnection.auth.dto.EmailSignUpTokenRequest; import com.example.solidconnection.auth.dto.EmailSignUpTokenResponse; +import com.example.solidconnection.auth.dto.ReissueRequest; import com.example.solidconnection.auth.dto.ReissueResponse; import com.example.solidconnection.auth.dto.SignInResponse; import com.example.solidconnection.auth.dto.SignUpRequest; @@ -114,13 +115,9 @@ public ResponseEntity quit( @PostMapping("/reissue") public ResponseEntity reissueToken( - Authentication authentication + ReissueRequest reissueRequest ) { - String accessToken = authentication.getCredentials().toString(); - if (accessToken == null) { - throw new CustomException(ErrorCode.AUTHENTICATION_FAILED, "토큰이 없습니다."); - } - ReissueResponse reissueResponse = authService.reissue(accessToken); + ReissueResponse reissueResponse = authService.reissue(reissueRequest); return ResponseEntity.ok(reissueResponse); } } diff --git a/src/main/java/com/example/solidconnection/auth/dto/ReissueRequest.java b/src/main/java/com/example/solidconnection/auth/dto/ReissueRequest.java new file mode 100644 index 000000000..00443255d --- /dev/null +++ b/src/main/java/com/example/solidconnection/auth/dto/ReissueRequest.java @@ -0,0 +1,8 @@ +package com.example.solidconnection.auth.dto; + +import jakarta.validation.constraints.NotBlank; + +public record ReissueRequest( + @NotBlank(message = "리프레시 토큰과 함께 요청해주세요.") + String refreshToken) { +} 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 486e01769..289893dbf 100644 --- a/src/main/java/com/example/solidconnection/auth/service/AuthService.java +++ b/src/main/java/com/example/solidconnection/auth/service/AuthService.java @@ -1,6 +1,7 @@ package com.example.solidconnection.auth.service; +import com.example.solidconnection.auth.dto.ReissueRequest; import com.example.solidconnection.auth.dto.ReissueResponse; import com.example.solidconnection.config.security.JwtProperties; import com.example.solidconnection.custom.exception.CustomException; @@ -10,6 +11,7 @@ import org.springframework.transaction.annotation.Transactional; import java.time.LocalDate; +import java.util.Objects; import java.util.Optional; import static com.example.solidconnection.custom.exception.ErrorCode.REFRESH_TOKEN_EXPIRED; @@ -43,18 +45,19 @@ public void quit(SiteUser siteUser) { /* * 액세스 토큰을 재발급한다. - * - 리프레시 토큰이 만료되었거나, 존재하지 않는다면 예외 응답을 반환한다. - * - 리프레시 토큰이 존재한다면, 액세스 토큰을 재발급한다. + * - 요청된 리프레시 토큰과 동일한 subject 의 토큰이 저장되어 있으며 값이 일치할 경우, 액세스 토큰을 재발급한다. + * - 그렇지 않으면 예외를 반환한다. * */ - public ReissueResponse reissue(String accessToken) { - // 리프레시 토큰 만료 확인 - String subject = parseSubject(accessToken, jwtProperties.secret()); - Optional optionalRefreshToken = authTokenProvider.findRefreshToken(subject); - if (optionalRefreshToken.isEmpty()) { + public ReissueResponse reissue(ReissueRequest reissueRequest) { + // 리프레시 토큰 확인 + String requestedRefreshToken = reissueRequest.refreshToken(); + String subject = parseSubject(requestedRefreshToken, jwtProperties.secret()); + Optional savedRefreshToken = authTokenProvider.findRefreshToken(subject); + if (!Objects.equals(requestedRefreshToken, savedRefreshToken.orElse(null))) { throw new CustomException(REFRESH_TOKEN_EXPIRED); } // 액세스 토큰 재발급 - String newAccessToken = authTokenProvider.generateAccessToken(accessToken); + String newAccessToken = authTokenProvider.generateAccessToken(subject); return new ReissueResponse(newAccessToken); } } From 0fb26cd335672be8ba6d99a3966dcfda3edf736f Mon Sep 17 00:00:00 2001 From: nayonsoso Date: Fri, 2 May 2025 15:14:01 +0900 Subject: [PATCH 7/7] =?UTF-8?q?test:=20authServiceTest=20=EC=9E=91?= =?UTF-8?q?=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../auth/service/AuthServiceTest.java | 108 ++++++++++++++++++ 1 file changed, 108 insertions(+) create mode 100644 src/test/java/com/example/solidconnection/auth/service/AuthServiceTest.java diff --git a/src/test/java/com/example/solidconnection/auth/service/AuthServiceTest.java b/src/test/java/com/example/solidconnection/auth/service/AuthServiceTest.java new file mode 100644 index 000000000..0030e45e2 --- /dev/null +++ b/src/test/java/com/example/solidconnection/auth/service/AuthServiceTest.java @@ -0,0 +1,108 @@ +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.config.security.JwtProperties; +import com.example.solidconnection.custom.exception.CustomException; +import com.example.solidconnection.siteuser.domain.SiteUser; +import com.example.solidconnection.siteuser.repository.SiteUserRepository; +import com.example.solidconnection.support.TestContainerSpringBootTest; +import com.example.solidconnection.type.PreparationStatus; +import com.example.solidconnection.type.Role; +import com.example.solidconnection.util.JwtUtils; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; + +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; + +@DisplayName("인증 서비스 테스트") +@TestContainerSpringBootTest +class AuthServiceTest { + + @Autowired + private AuthService authService; + + @Autowired + private AuthTokenProvider authTokenProvider; + + @Autowired + private SiteUserRepository siteUserRepository; + + @Autowired + private JwtProperties jwtProperties; + + @Test + void 로그아웃한다() { + // given + String accessToken = "accessToken"; + + // when + authService.signOut(accessToken); + + // then + assertThat(authTokenProvider.findBlackListToken(accessToken)).isNotNull(); + } + + @Test + void 탈퇴한다() { + // given + SiteUser siteUser = createSiteUser(); + + // when + authService.quit(siteUser); + + // then + LocalDate tomorrow = LocalDate.now().plusDays(1); + assertThat(siteUser.getQuitedAt()).isEqualTo(tomorrow); + } + + @Nested + class 토큰을_재발급한다 { + + @Test + void 요청의_리프레시_토큰이_저장되어_있고_값이_일치면_액세스_토큰을_재발급한다() { + // given + SiteUser siteUser = createSiteUser(); + String refreshToken = authTokenProvider.generateAndSaveRefreshToken(siteUser); + ReissueRequest reissueRequest = new ReissueRequest(refreshToken); + + // when + ReissueResponse reissuedAccessToken = authService.reissue(reissueRequest); + + // then + String actualSubject = JwtUtils.parseSubject(reissuedAccessToken.accessToken(), jwtProperties.secret()); + String expectedSubject = JwtUtils.parseSubject(refreshToken, jwtProperties.secret()); + assertThat(actualSubject).isEqualTo(expectedSubject); + } + + @Test + void 요청의_리프레시_토큰이_저장되어있지_않다면_예외_응답을_반환한다() { + // given + String refreshToken = authTokenProvider.generateToken("subject", TokenType.REFRESH); + ReissueRequest reissueRequest = new ReissueRequest(refreshToken); + + // when, then + assertThatCode(() -> authService.reissue(reissueRequest)) + .isInstanceOf(CustomException.class) + .hasMessage(REFRESH_TOKEN_EXPIRED.getMessage()); + } + } + + private SiteUser createSiteUser() { + SiteUser siteUser = new SiteUser( + "test@example.com", + "nickname", + "profileImageUrl", + PreparationStatus.CONSIDERING, + Role.MENTEE + ); + return siteUserRepository.save(siteUser); + } +}