Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
import javax.crypto.SecretKey;
import java.nio.charset.StandardCharsets;
import java.util.Date;
import java.util.UUID;

@Component
public class JwtTokenProvider {
Expand Down Expand Up @@ -51,6 +52,7 @@ private String buildToken(String username, Long userId, UserRole role, long vali
Date validity = new Date(now.getTime() + validityInMilliseconds);

JwtBuilder builder = Jwts.builder()
.id(UUID.randomUUID().toString())
.subject(username)
.claim("userId", userId)
.issuedAt(now)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,12 @@

import me.thinkcat.opic.practice.entity.RefreshToken;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Modifying;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;
import org.springframework.stereotype.Repository;

import java.time.LocalDateTime;
import java.util.Optional;

@Repository
Expand All @@ -14,4 +18,8 @@ public interface RefreshTokenRepository extends JpaRepository<RefreshToken, Long
void deleteByToken(String token);

void deleteByUserId(Long userId);

@Modifying
@Query("DELETE FROM RefreshToken r WHERE r.expiresAt < :now")
void deleteAllExpired(@Param("now") LocalDateTime now);
}
Original file line number Diff line number Diff line change
Expand Up @@ -43,4 +43,11 @@ public void hardDeleteWithdrawnUsers() {

log.info("[UserCleanup] Hard deleted {} withdrawn users.", targets.size());
}

@Scheduled(cron = "0 0 4 * * *")
@Transactional
public void cleanupExpiredRefreshTokens() {
refreshTokenRepository.deleteAllExpired(LocalDateTime.now());
log.info("[UserCleanup] Expired refresh tokens deleted.");
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
package me.thinkcat.opic.practice.config.security;

import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.springframework.test.util.ReflectionTestUtils;

import java.util.HashSet;
import java.util.Set;

import static org.assertj.core.api.Assertions.assertThat;

class JwtTokenProviderTest {

private JwtTokenProvider jwtTokenProvider;

@BeforeEach
void setUp() {
jwtTokenProvider = new JwtTokenProvider();
ReflectionTestUtils.setField(jwtTokenProvider, "secretKey", "testsecretkeyfortestingpurposesonly12345678");
ReflectionTestUtils.setField(jwtTokenProvider, "accessTokenValidity", 3600000L);
ReflectionTestUtils.setField(jwtTokenProvider, "refreshTokenValidity", 1209600000L);
jwtTokenProvider.init();
}

@Test
void 동일_사용자_동일_시점에_generateRefreshToken_연속_호출시_서로다른_토큰_반환() {
String token1 = jwtTokenProvider.generateRefreshToken("user", 1L);
String token2 = jwtTokenProvider.generateRefreshToken("user", 1L);

assertThat(token1).isNotEqualTo(token2);
}

@Test
void generateRefreshToken_100회_호출시_모두_고유한_토큰_반환() {
Set<String> tokens = new HashSet<>();
for (int i = 0; i < 100; i++) {
tokens.add(jwtTokenProvider.generateRefreshToken("user", 1L));
}

assertThat(tokens).hasSize(100);
}
}
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
package me.thinkcat.opic.practice.service;

import me.thinkcat.opic.practice.config.security.JwtTokenProvider;
import me.thinkcat.opic.practice.dto.request.LoginRequest;
import me.thinkcat.opic.practice.dto.request.UserRegisterRequest;
import me.thinkcat.opic.practice.dto.response.TokenResponse;
import me.thinkcat.opic.practice.dto.response.UserResponse;
import me.thinkcat.opic.practice.entity.RefreshToken;
import me.thinkcat.opic.practice.entity.User;
import me.thinkcat.opic.practice.exception.ValidationException;
import me.thinkcat.opic.practice.repository.UserRepository;
Expand All @@ -17,6 +20,7 @@
import org.springframework.security.crypto.password.PasswordEncoder;

import java.time.LocalDateTime;
import java.util.Optional;

import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
Expand Down Expand Up @@ -62,6 +66,25 @@ class UserServiceTest {
assertThat(response.getUsername()).isEqualTo("user");
}

@Test
void 동일_사용자_연속_두번_로그인시_서로다른_refreshToken_반환() {
User user = User.builder().id(1L).username("user").build();
RefreshToken firstToken = RefreshToken.builder().token("refresh-token-aaa").build();
RefreshToken secondToken = RefreshToken.builder().token("refresh-token-bbb").build();

given(userRepository.findByUsername("user")).willReturn(Optional.of(user));
given(jwtTokenProvider.generateAccessToken(any(), any(), any())).willReturn("access-token");
given(jwtTokenProvider.getAccessTokenValidityInSeconds()).willReturn(3600L);
given(refreshTokenService.createRefreshToken(user))
.willReturn(firstToken)
.willReturn(secondToken);

TokenResponse response1 = userService.login(new LoginRequest("user", "Password1@"));
TokenResponse response2 = userService.login(new LoginRequest("user", "Password1@"));

assertThat(response1.getRefreshToken()).isNotEqualTo(response2.getRefreshToken());
}

@ParameterizedTest
@ValueSource(strings = {
"notanemail",
Expand Down