diff --git a/account-service/account-service/build.gradle b/account-service/account-service/build.gradle index 84f58e4..a0fc512 100644 --- a/account-service/account-service/build.gradle +++ b/account-service/account-service/build.gradle @@ -4,6 +4,13 @@ repositories { dependencies { implementation project(':account-service-api') + + //Spring Actuator + implementation 'org.springframework.boot:spring-boot-starter-actuator' + //Spring Prometheus + implementation 'io.micrometer:micrometer-registry-prometheus' + //Redis + implementation 'org.springframework.boot:spring-boot-starter-data-redis' } def generated = 'src/main/generated' diff --git a/account-service/account-service/src/main/java/com/synapse/account_service/AccountServiceConfig.java b/account-service/account-service/src/main/java/com/synapse/account_service/AccountServiceConfig.java index b1065a5..a68467c 100644 --- a/account-service/account-service/src/main/java/com/synapse/account_service/AccountServiceConfig.java +++ b/account-service/account-service/src/main/java/com/synapse/account_service/AccountServiceConfig.java @@ -3,7 +3,9 @@ import org.springframework.boot.autoconfigure.EnableAutoConfiguration; import org.springframework.context.annotation.ComponentScan; import org.springframework.data.jpa.repository.config.EnableJpaAuditing; +import org.springframework.scheduling.annotation.EnableAsync; +@EnableAsync @EnableJpaAuditing @ComponentScan @EnableAutoConfiguration diff --git a/account-service/account-service/src/main/java/com/synapse/account_service/configuration/RedisConfig.java b/account-service/account-service/src/main/java/com/synapse/account_service/configuration/RedisConfig.java new file mode 100644 index 0000000..4706933 --- /dev/null +++ b/account-service/account-service/src/main/java/com/synapse/account_service/configuration/RedisConfig.java @@ -0,0 +1,32 @@ +package com.synapse.account_service.configuration; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.data.redis.connection.RedisConnectionFactory; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer; +import org.springframework.data.redis.serializer.StringRedisSerializer; + +import com.fasterxml.jackson.databind.DeserializationFeature; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.SerializationFeature; +import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; +import com.synapse.account_service.domain.RefreshToken; + +@Configuration +public class RedisConfig { + @Bean + RedisTemplate refreshTokenRedisTemplate(RedisConnectionFactory connectionFactory) { + var objectMapper = new ObjectMapper() + .configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false) + .registerModule(new JavaTimeModule()) + .disable(SerializationFeature.WRITE_DATE_KEYS_AS_TIMESTAMPS); + + var template = new RedisTemplate(); + template.setConnectionFactory(connectionFactory); + template.setKeySerializer(new StringRedisSerializer()); + template.setValueSerializer(new Jackson2JsonRedisSerializer<>(objectMapper, RefreshToken.class)); + + return template; + } +} diff --git a/account-service/account-service/src/main/java/com/synapse/account_service/configuration/SecurityConfig.java b/account-service/account-service/src/main/java/com/synapse/account_service/configuration/SecurityConfig.java index f514a5a..cffa34a 100644 --- a/account-service/account-service/src/main/java/com/synapse/account_service/configuration/SecurityConfig.java +++ b/account-service/account-service/src/main/java/com/synapse/account_service/configuration/SecurityConfig.java @@ -33,9 +33,9 @@ public class SecurityConfig { private final CustomUserDetailsService customUserDetailsService; private final LoginSuccessHandler loginSuccessHandler; private final LoginFailureHandler loginFailureHandler; + private final ObjectMapper objectMapper; private final CustomOAuth2UserService customOAuth2UserService; private final CustomOidcUserService customOidcUserService; - private final ObjectMapper objectMapper; private final PasswordEncoder passwordEncoder; @@ -57,7 +57,9 @@ public SecurityFilterChain securityFilterChain(HttpSecurity http, JwtAuthenticat .authorizeHttpRequests(auth -> auth .requestMatchers("/api/accounts/signup", "/api/accounts/login", "/", - "/api/accounts/token/reissue") + "/api/accounts/token/reissue", + "/actuator/health", "/actuator/info", + "/actuator/prometheus", "/actuator/metrics", "/actuator/mappings") .permitAll() .anyRequest().authenticated()) .addFilterAt(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class) diff --git a/account-service/account-service/src/main/java/com/synapse/account_service/controller/AccountController.java b/account-service/account-service/src/main/java/com/synapse/account_service/controller/AccountController.java index 1509e4d..a3ba6f4 100644 --- a/account-service/account-service/src/main/java/com/synapse/account_service/controller/AccountController.java +++ b/account-service/account-service/src/main/java/com/synapse/account_service/controller/AccountController.java @@ -11,7 +11,6 @@ import com.synapse.account_service.service.AccountService; import com.synapse.account_service_api.dto.request.SignUpRequest; import com.synapse.account_service_api.dto.response.SignUpResponse; - import jakarta.validation.Valid; import lombok.RequiredArgsConstructor; diff --git a/account-service/account-service/src/main/java/com/synapse/account_service/domain/RefreshToken.java b/account-service/account-service/src/main/java/com/synapse/account_service/domain/RefreshToken.java new file mode 100644 index 0000000..2b0fb13 --- /dev/null +++ b/account-service/account-service/src/main/java/com/synapse/account_service/domain/RefreshToken.java @@ -0,0 +1,18 @@ +package com.synapse.account_service.domain; + +import java.util.UUID; + +import lombok.*; + +@Getter +@NoArgsConstructor(access = AccessLevel.PROTECTED) +public class RefreshToken { + private UUID memberId; + private String token; + + @Builder + public RefreshToken(UUID memberId, String token) { + this.memberId = memberId; + this.token = token; + } +} diff --git a/account-service/account-service/src/main/java/com/synapse/account_service/domain/entity/Member.java b/account-service/account-service/src/main/java/com/synapse/account_service/domain/entity/Member.java index ece378c..52d9c25 100644 --- a/account-service/account-service/src/main/java/com/synapse/account_service/domain/entity/Member.java +++ b/account-service/account-service/src/main/java/com/synapse/account_service/domain/entity/Member.java @@ -17,7 +17,9 @@ @Getter @Entity @NoArgsConstructor(access = AccessLevel.PROTECTED) -@Table(name = "members") +@Table(name = "members", indexes = { + @Index(name = "idx_member_username", columnList = "username") +}) public class Member extends BaseEntity { @Id diff --git a/account-service/account-service/src/main/java/com/synapse/account_service/domain/entity/RefreshToken.java b/account-service/account-service/src/main/java/com/synapse/account_service/domain/entity/RefreshToken.java deleted file mode 100644 index 61e34eb..0000000 --- a/account-service/account-service/src/main/java/com/synapse/account_service/domain/entity/RefreshToken.java +++ /dev/null @@ -1,31 +0,0 @@ -package com.synapse.account_service.domain.entity; - -import java.util.UUID; - -import com.synapse.account_service.domain.common.BaseTimeEntity; - -import jakarta.persistence.*; -import lombok.*; - -@Entity -@Getter -@NoArgsConstructor(access = AccessLevel.PROTECTED) -@Table(name = "refresh_token") -public class RefreshToken extends BaseTimeEntity { - @Id - @Column(columnDefinition = "uuid") - private UUID memberId; - - @Column(nullable = false, length = 512) - private String token; - - @Builder - public RefreshToken(UUID memberId, String token) { - this.memberId = memberId; - this.token = token; - } - - public void updateToken(String newToken) { - this.token = newToken; - } -} diff --git a/account-service/account-service/src/main/java/com/synapse/account_service/domain/repository/RefreshTokenRepository.java b/account-service/account-service/src/main/java/com/synapse/account_service/domain/repository/RefreshTokenRepository.java deleted file mode 100644 index afc9c30..0000000 --- a/account-service/account-service/src/main/java/com/synapse/account_service/domain/repository/RefreshTokenRepository.java +++ /dev/null @@ -1,11 +0,0 @@ -package com.synapse.account_service.domain.repository; - -import java.util.UUID; - -import org.springframework.data.jpa.repository.JpaRepository; - -import com.synapse.account_service.domain.entity.RefreshToken; - -public interface RefreshTokenRepository extends JpaRepository { - -} diff --git a/account-service/account-service/src/main/java/com/synapse/account_service/service/AccountService.java b/account-service/account-service/src/main/java/com/synapse/account_service/service/AccountService.java index 9732e94..05bfb56 100644 --- a/account-service/account-service/src/main/java/com/synapse/account_service/service/AccountService.java +++ b/account-service/account-service/src/main/java/com/synapse/account_service/service/AccountService.java @@ -18,6 +18,7 @@ import com.synapse.account_service.exception.ExceptionType; import com.synapse.account_service_api.dto.request.SignUpRequest; import com.synapse.account_service_api.dto.response.SignUpResponse; + import com.synapse.account_service_api.event.MemberDomainEvent; import io.eventuate.tram.events.aggregates.ResultWithDomainEvents; @@ -32,6 +33,7 @@ public class AccountService { private final MemberRepository memberRepository; private final PasswordEncoder passwordEncoder; + // private final MemberDomainEventPublisher memberDomainEventPublisher; @Transactional diff --git a/account-service/account-service/src/main/java/com/synapse/account_service/service/CustomUserDetailsService.java b/account-service/account-service/src/main/java/com/synapse/account_service/service/CustomUserDetailsService.java index bff117b..094ab89 100644 --- a/account-service/account-service/src/main/java/com/synapse/account_service/service/CustomUserDetailsService.java +++ b/account-service/account-service/src/main/java/com/synapse/account_service/service/CustomUserDetailsService.java @@ -23,6 +23,6 @@ public UserDetails loadUserByUsername(String username) throws UsernameNotFoundEx ProviderUserRequest providerUserRequest = new ProviderUserRequest(member); ProviderUser providerUser = providerUser(providerUserRequest); - return new PrincipalUser(providerUser); + return new PrincipalUser(providerUser, member); } } diff --git a/account-service/account-service/src/main/java/com/synapse/account_service/service/TokenManagementService.java b/account-service/account-service/src/main/java/com/synapse/account_service/service/TokenManagementService.java index bd648f6..aea06b6 100644 --- a/account-service/account-service/src/main/java/com/synapse/account_service/service/TokenManagementService.java +++ b/account-service/account-service/src/main/java/com/synapse/account_service/service/TokenManagementService.java @@ -1,14 +1,18 @@ package com.synapse.account_service.service; +import java.time.Duration; +import java.time.Instant; +import java.time.temporal.ChronoUnit; +import java.util.concurrent.TimeUnit; import java.util.UUID; +import org.springframework.data.redis.core.RedisTemplate; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; +import com.synapse.account_service.domain.RefreshToken; import com.synapse.account_service.domain.entity.Member; -import com.synapse.account_service.domain.entity.RefreshToken; import com.synapse.account_service.domain.repository.MemberRepository; -import com.synapse.account_service.domain.repository.RefreshTokenRepository; import com.synapse.account_service.exception.ExceptionType; import com.synapse.account_service.exception.JWTValidationException; import com.synapse.account_service.exception.NotFoundException; @@ -22,29 +26,28 @@ @RequiredArgsConstructor public class TokenManagementService { private final JwtTokenService jwtTokenService; - private final RefreshTokenRepository refreshTokenRepository; private final MemberRepository memberRepository; + private final RedisTemplate refreshTokenRedisTemplate; public void saveOrUpdateRefreshToken(UUID memberId, TokenResult refreshToken) { - refreshTokenRepository.findById(memberId) - .ifPresentOrElse( - // 기존 토큰이 있으면, 새 토큰으로 값을 업데이트 (재로그인 시) - existingToken -> existingToken.updateToken(refreshToken.token()), - // 기존 토큰이 없으면, 새로 생성하여 저장 (최초 로그인) - () -> { - RefreshToken newRefreshToken = new RefreshToken(memberId, refreshToken.token()); - refreshTokenRepository.save(newRefreshToken); - }); + String redisKey = "refresh_token:" + memberId.toString(); + RefreshToken refreshTokenEntity = new RefreshToken(memberId, refreshToken.token()); + + long ttlSeconds = ChronoUnit.SECONDS.between(Instant.now(), refreshToken.expiresAt()); + refreshTokenRedisTemplate.opsForValue().set(redisKey, refreshTokenEntity, ttlSeconds, TimeUnit.SECONDS); } public TokenResponse reissueTokens(String requestRefreshToken) { UUID memberId = jwtTokenService.getMemberIdFrom(requestRefreshToken); + String redisKey = "refresh_token:" + memberId.toString(); - RefreshToken storedToken = refreshTokenRepository.findById(memberId) - .orElseThrow(() -> new JWTValidationException(ExceptionType.INVALID_REFRESH_TOKEN)); + RefreshToken storedToken = refreshTokenRedisTemplate.opsForValue().get(redisKey); + if (storedToken == null) { + throw new JWTValidationException(ExceptionType.INVALID_REFRESH_TOKEN); + } if (!storedToken.getToken().equals(requestRefreshToken)) { - refreshTokenRepository.delete(storedToken); + refreshTokenRedisTemplate.delete(redisKey); throw new JWTValidationException(ExceptionType.TAMPERED_REFRESH_TOKEN); } @@ -55,7 +58,10 @@ public TokenResponse reissueTokens(String requestRefreshToken) { TokenResponse newTokens = jwtTokenService.createTokenResponse(memberId.toString(), role); - storedToken.updateToken(newTokens.refreshToken().token()); + // Redis에 새로운 RefreshToken 저장 + RefreshToken newRefreshToken = new RefreshToken(memberId, newTokens.refreshToken().token()); + long ttlSeconds = Duration.between(Instant.now(), newTokens.refreshToken().expiresAt()).getSeconds(); + refreshTokenRedisTemplate.opsForValue().set(redisKey, newRefreshToken, ttlSeconds, TimeUnit.SECONDS); return newTokens; } diff --git a/account-service/account-service/src/main/resources/application-local.yml b/account-service/account-service/src/main/resources/application-local.yml index a5583f6..4dc757a 100644 --- a/account-service/account-service/src/main/resources/application-local.yml +++ b/account-service/account-service/src/main/resources/application-local.yml @@ -4,6 +4,19 @@ spring: url: jdbc:postgresql://${local-db.postgres.host}:${local-db.postgres.port}/${local-db.postgres.name} username: ${local-db.postgres.username} password: ${local-db.postgres.password} + hikari: + maximum-pool-size: 70 + minimum-idle: 10 + + data: + redis: + host: ${local-redis.host} + port: ${local-redis.port} + lettuce: + pool: + max-active: 32 + max-idle: 16 + min-idle: 8 jpa: properties: @@ -13,8 +26,13 @@ spring: highlight: sql: true hbm2ddl: - auto: create + auto: update dialect: org.hibernate.dialect.PostgreSQLDialect + jdbc: + batch_size: 30 + order_inserts: true + order_updates: true + default_batch_fetch_size: 100 open-in-view: false show-sql: true diff --git a/account-service/account-service/src/main/resources/application.yml b/account-service/account-service/src/main/resources/application.yml index c9a94eb..838e6dc 100644 --- a/account-service/account-service/src/main/resources/application.yml +++ b/account-service/account-service/src/main/resources/application.yml @@ -1,5 +1,8 @@ server: port: 1001 + tomcat: + mbeanregistry: + enabled: true spring: main: @@ -16,3 +19,21 @@ spring: - security/application-db.yml - security/application-jwt.yml - security/application-oauth2.yml + +management: + server: + port: 6001 + endpoints: + web: + exposure: + include: health,info,prometheus,mappings, metrics + endpoint: + health: + show-details: always + prometheus: + metrics: + export: + enabled: true + metrics: + tags: + application: ${spring.application.name} diff --git a/account-service/account-service/src/main/resources/db/QueryPlan.sql b/account-service/account-service/src/main/resources/db/QueryPlan.sql new file mode 100644 index 0000000..5b3a6ed --- /dev/null +++ b/account-service/account-service/src/main/resources/db/QueryPlan.sql @@ -0,0 +1,3 @@ +# 로그인 시 username 조회 쿼리 플랜 +EXPLAIN SELECT * FROM members WHERE username = 'test'; + diff --git a/account-service/account-service/src/test/java/com/synapse/account_service/integrationtest/LoginIntegrationTest.java b/account-service/account-service/src/test/java/com/synapse/account_service/integrationtest/LoginIntegrationTest.java index 6870799..441ade8 100644 --- a/account-service/account-service/src/test/java/com/synapse/account_service/integrationtest/LoginIntegrationTest.java +++ b/account-service/account-service/src/test/java/com/synapse/account_service/integrationtest/LoginIntegrationTest.java @@ -3,6 +3,9 @@ import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +import java.util.UUID; + import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.cookie; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; @@ -41,14 +44,16 @@ public class LoginIntegrationTest extends TestConfig{ @BeforeEach void setUp() { + memberRepository.deleteAll(); Member testMember = Member.builder() + .id(UUID.randomUUID()) .email("test_user1234@example.com") .username(TEST_USERNAME) .password(passwordEncoder.encode(TEST_PASSWORD)) .role(MemberRole.USER) .provider("local") .build(); - memberRepository.save(testMember); + testMember = memberRepository.save(testMember); } @Test diff --git a/account-service/account-service/src/test/java/com/synapse/account_service/integrationtest/TokenReissueIntegrationTest.java b/account-service/account-service/src/test/java/com/synapse/account_service/integrationtest/TokenReissueIntegrationTest.java index 1c00e4d..6ecfa3a 100644 --- a/account-service/account-service/src/test/java/com/synapse/account_service/integrationtest/TokenReissueIntegrationTest.java +++ b/account-service/account-service/src/test/java/com/synapse/account_service/integrationtest/TokenReissueIntegrationTest.java @@ -3,6 +3,9 @@ import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +import java.util.UUID; + import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.cookie; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; @@ -15,11 +18,10 @@ import com.synapse.account_service.TestConfig; import com.synapse.account_service.domain.entity.Member; -import com.synapse.account_service.domain.entity.RefreshToken; import com.synapse.account_service.domain.enums.MemberRole; import com.synapse.account_service.domain.repository.MemberRepository; -import com.synapse.account_service.domain.repository.RefreshTokenRepository; import com.synapse.account_service.service.JwtTokenService; +import com.synapse.account_service.service.TokenManagementService; import com.synapse.account_service_api.dto.response.TokenResponse; import jakarta.servlet.http.Cookie; @@ -33,10 +35,10 @@ public class TokenReissueIntegrationTest extends TestConfig { private JwtTokenService jwtTokenService; @Autowired - private RefreshTokenRepository refreshTokenRepository; + private MemberRepository memberRepository; @Autowired - private MemberRepository memberRepository; + private TokenManagementService tokenManagementService; private Member testMember; private String validRefreshToken; @@ -45,18 +47,19 @@ public class TokenReissueIntegrationTest extends TestConfig { void setUp() { // 테스트용 사용자 생성 및 저장 testMember = Member.builder() + .id(UUID.randomUUID()) .email("reissue_user@example.com") .username("reissue_user") .password("password") .role(MemberRole.USER) .provider("local") .build(); - memberRepository.save(testMember); + testMember = memberRepository.save(testMember); - // 테스트용 유효한 리프레시 토큰 생성 및 DB에 저장 + // 테스트용 유효한 리프레시 토큰 생성 및 Redis에 저장 TokenResponse tokens = jwtTokenService.createTokenResponse(testMember.getId().toString(), "USER"); validRefreshToken = tokens.refreshToken().token(); - refreshTokenRepository.save(new RefreshToken(testMember.getId(), validRefreshToken)); + tokenManagementService.saveOrUpdateRefreshToken(testMember.getId(), tokens.refreshToken()); } @Test diff --git a/account-service/account-service/src/test/java/com/synapse/account_service/repository/MemberRepositoryTest.java b/account-service/account-service/src/test/java/com/synapse/account_service/repository/MemberRepositoryTest.java index a21160a..b45803d 100644 --- a/account-service/account-service/src/test/java/com/synapse/account_service/repository/MemberRepositoryTest.java +++ b/account-service/account-service/src/test/java/com/synapse/account_service/repository/MemberRepositoryTest.java @@ -4,6 +4,7 @@ import static org.junit.jupiter.api.Assertions.assertThrows; import java.util.Optional; +import java.util.UUID; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; @@ -32,6 +33,7 @@ public class MemberRepositoryTest extends TestConfig { @BeforeEach void setUp() { testMember = Member.builder() + .id(UUID.randomUUID()) .email("test@example.com") .password("encrypted_password") .username("테스트유저") @@ -64,6 +66,7 @@ void save_withDuplicateEmail_shouldThrowException() { // when: 동일한 이메일을 가진 새로운 회원을 만듭니다. Member duplicateMember = Member.builder() + .id(UUID.randomUUID()) .email("test@example.com") // 중복된 이메일 .password("another_password") .role(MemberRole.USER) @@ -104,6 +107,7 @@ void save_shouldSetCreatedAt() { void findByProviderAndRegistrationId_shouldReturnMember() { // given Member oauthMember = Member.builder() + .id(UUID.randomUUID()) .email("google_user@example.com") .password("social_login_password") // 실제로는 비밀번호가 없을 수도 있습니다. .username("구글유저") diff --git a/account-service/account-service/src/test/java/com/synapse/account_service/service/TokenManagementServiceTest.java b/account-service/account-service/src/test/java/com/synapse/account_service/service/TokenManagementServiceTest.java deleted file mode 100644 index fe92e34..0000000 --- a/account-service/account-service/src/test/java/com/synapse/account_service/service/TokenManagementServiceTest.java +++ /dev/null @@ -1,107 +0,0 @@ -package com.synapse.account_service.service; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.junit.jupiter.api.Assertions.assertThrows; -import static org.mockito.BDDMockito.given; -import static org.mockito.Mockito.verify; - -import java.time.Instant; -import java.util.Optional; -import java.util.UUID; - -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.DisplayName; -import org.junit.jupiter.api.Test; -import org.mockito.InjectMocks; -import org.mockito.Mock; - -import com.synapse.account_service.TestConfig; -import com.synapse.account_service.domain.entity.Member; -import com.synapse.account_service.domain.entity.RefreshToken; -import com.synapse.account_service.domain.enums.MemberRole; -import com.synapse.account_service.domain.repository.MemberRepository; -import com.synapse.account_service.domain.repository.RefreshTokenRepository; -import com.synapse.account_service.exception.JWTValidationException; -import com.synapse.account_service_api.dto.TokenResult; -import com.synapse.account_service_api.dto.response.TokenResponse; - -public class TokenManagementServiceTest extends TestConfig { - - @InjectMocks - private TokenManagementService tokenManagementService; - - @Mock - private JwtTokenService jwtTokenService; - @Mock - private RefreshTokenRepository refreshTokenRepository; - @Mock - private MemberRepository memberRepository; - - private UUID memberId; - private String validRefreshToken; - - @BeforeEach - void setUp() { - memberId = UUID.randomUUID(); - validRefreshToken = "valid.refresh.token"; - } - - @Test - @DisplayName("토큰 재발급 성공: 유효한 리프레시 토큰으로 요청 시, 새로운 토큰 쌍을 반환하고 DB를 갱신한다") - void reissueTokens_success() { - // given - RefreshToken storedToken = new RefreshToken(memberId, validRefreshToken); - Member member = Member.builder() - .role(MemberRole.USER) - .build(); - TokenResponse newTokens = new TokenResponse( - new TokenResult("new.access.token", Instant.now().plusSeconds(1800)), - new TokenResult("new.refresh.token", Instant.now().plusSeconds(86400)) - ); - - given(jwtTokenService.getMemberIdFrom(validRefreshToken)).willReturn(memberId); - given(refreshTokenRepository.findById(memberId)).willReturn(Optional.of(storedToken)); - given(memberRepository.findById(memberId)).willReturn(Optional.of(member)); - given(jwtTokenService.createTokenResponse(memberId.toString(), "USER")).willReturn(newTokens); - - // when - TokenResponse result = tokenManagementService.reissueTokens(validRefreshToken); - - // then - assertThat(result.accessToken().token()).isEqualTo("new.access.token"); - assertThat(storedToken.getToken()).isEqualTo("new.refresh.token"); // Rotation 검증 - verify(refreshTokenRepository).findById(memberId); - verify(memberRepository).findById(memberId); - } - - @Test - @DisplayName("토큰 재발급 실패: DB에 저장된 토큰과 일치하지 않으면 InvalidTokenException을 던지고 DB에서 삭제한다 (탈취 의심)") - void reissueTokens_fail_whenTokenMismatched() { - // given - RefreshToken storedToken = new RefreshToken(memberId, "different.token.in.db"); - - given(jwtTokenService.getMemberIdFrom(validRefreshToken)).willReturn(memberId); - given(refreshTokenRepository.findById(memberId)).willReturn(Optional.of(storedToken)); - - // when & then - assertThrows(JWTValidationException.class, () -> { - tokenManagementService.reissueTokens(validRefreshToken); - }); - - // 탈취 시도로 간주하고, DB에서 해당 토큰을 삭제했는지 검증 - verify(refreshTokenRepository).delete(storedToken); - } - - @Test - @DisplayName("토큰 재발급 실패: DB에 리프레시 토큰이 없으면 InvalidTokenException을 던진다") - void reissueTokens_fail_whenTokenNotFoundInDb() { - // given - given(jwtTokenService.getMemberIdFrom(validRefreshToken)).willReturn(memberId); - given(refreshTokenRepository.findById(memberId)).willReturn(Optional.empty()); - - // when & then - assertThrows(JWTValidationException.class, () -> { - tokenManagementService.reissueTokens(validRefreshToken); - }); - } -}