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 @@ -12,6 +12,7 @@
import com.techfork.domain.user.repository.UserRepository;
import com.techfork.global.exception.GeneralException;
import com.techfork.global.security.auth.service.RefreshTokenService;
import com.techfork.global.security.auth.service.UserAuthCacheService;
import com.techfork.global.security.jwt.JwtDTO;
import com.techfork.global.security.jwt.JwtProperties;
import com.techfork.global.security.jwt.JwtUtil;
Expand All @@ -37,6 +38,7 @@ public class AuthService {
private final JwtProperties jwtProperties;
private final AuthConverter authConverter;
private final KakaoOAuthService kakaoOAuthService;
private final UserAuthCacheService userAuthCacheService;

@Value("${server.domain}")
private String domain;
Expand All @@ -54,7 +56,9 @@ public TokenRefreshResponse refreshToken(String refreshToken, HttpServletRespons
long expiration = jwtProperties.getRefreshTokenExpiration();
saveAndSetRefreshToken(response, userId, newTokens.refreshToken(), expiration);

log.info("Token refreshed for userId: {}", userId);
userAuthCacheService.put(userId, user, jwtProperties.getAccessTokenExpiration());

log.info("Token refreshed");

return TokenRefreshResponse.builder()
.accessToken(newTokens.accessToken())
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
import com.techfork.domain.user.exception.UserErrorCode;
import com.techfork.domain.user.repository.UserRepository;
import com.techfork.global.exception.GeneralException;
import com.techfork.global.security.auth.service.UserAuthCacheService;
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
Expand All @@ -21,6 +22,7 @@ public class UserCommandService {

private final InterestCommandService interestCommandService;
private final UserRepository userRepository;
private final UserAuthCacheService userAuthCacheService;

public void completeOnboarding(Long userId, @Valid OnboardingRequest request) {
User user = userRepository.findByIdWithInterestCategories(userId)
Expand All @@ -29,6 +31,8 @@ public void completeOnboarding(Long userId, @Valid OnboardingRequest request) {
user.updateUser(request.nickname(), request.email(), request.description());

interestCommandService.saveUserInterests(user, new SaveInterestRequest(request.interests()));

userAuthCacheService.evict(userId);
}

public void updateUserProfile(Long userId, UpdateUserProfileRequest request) {
Expand All @@ -52,7 +56,8 @@ public void withdrawUser(Long userId) {
}

user.withdraw();
userAuthCacheService.evict(userId);

log.info("User withdrawn - userId: {}, status changed to WITHDRAWN and personal data anonymized", userId);
log.info("User withdrawn - status changed to WITHDRAWN and personal data anonymized");
}
}
1 change: 1 addition & 0 deletions src/main/java/com/techfork/global/constant/RedisKey.java
Original file line number Diff line number Diff line change
Expand Up @@ -4,5 +4,6 @@ public final class RedisKey {
private RedisKey() {}

public static final String REFRESH_TOKEN_PREFIX = "refreshToken:";
public static final String USER_AUTH_PREFIX = "user:auth:";
public static final String CRAWLING_LOCK_KEY = "rss-crawling";
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
package com.techfork.global.security.auth.service;

import com.techfork.domain.user.entity.User;
import com.techfork.domain.user.enums.Role;
import com.techfork.domain.user.enums.UserStatus;
import com.techfork.global.constant.RedisKey;
import com.techfork.global.security.oauth.UserPrincipal;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Service;

import java.util.concurrent.TimeUnit;

@Slf4j
@Service
@RequiredArgsConstructor
public class UserAuthCacheService {

private static final String DELIMITER = "|";
private static final int FIELD_COUNT = 4;

private final StringRedisTemplate redisTemplate;

public UserPrincipal get(Long userId) {
String key = buildKey(userId);
String cached = redisTemplate.opsForValue().get(key);
if (cached == null) {
return null;
}
return deserialize(cached);
}

public void put(Long userId, User user, long ttlMillis) {
String key = buildKey(userId);
String value = serialize(user);
redisTemplate.opsForValue().set(key, value, ttlMillis, TimeUnit.MILLISECONDS);
log.debug("User auth cached for userId: {}", userId);
}

public void evict(Long userId) {
String key = buildKey(userId);
redisTemplate.delete(key);
log.debug("User auth cache evicted for userId: {}", userId);
}

private String buildKey(Long userId) {
return RedisKey.USER_AUTH_PREFIX + userId;
}

private String serialize(User user) {
return user.getId()
+ DELIMITER + user.getRole().name()
+ DELIMITER + user.getStatus().name()
+ DELIMITER + (user.getEmail() != null ? user.getEmail() : "");
}

private UserPrincipal deserialize(String value) {
String[] parts = value.split("\\" + DELIMITER, FIELD_COUNT);
if (parts.length != FIELD_COUNT) {
log.warn("Invalid user auth cache format: {}", value);
return null;
}
return UserPrincipal.builder()
.id(Long.parseLong(parts[0]))
.role(Role.valueOf(parts[1]))
.status(UserStatus.valueOf(parts[2]))
.email(parts[3].isEmpty() ? null : parts[3])
.build();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,13 @@

import com.techfork.domain.auth.exception.AuthErrorCode;
import com.techfork.domain.user.entity.User;
import com.techfork.domain.user.enums.UserStatus;
import com.techfork.domain.user.repository.UserRepository;
import com.techfork.global.constant.Constants;
import com.techfork.global.constant.MdcKey;
import com.techfork.global.exception.GeneralException;
import com.techfork.global.security.auth.service.UserAuthCacheService;
import com.techfork.global.security.jwt.JwtProperties;
import com.techfork.global.security.jwt.JwtUtil;
import com.techfork.global.security.oauth.UserPrincipal;
import com.techfork.global.util.HeaderUtil;
Expand Down Expand Up @@ -40,6 +43,8 @@ public class JwtAuthenticationFilter extends OncePerRequestFilter {

private final JwtUtil jwtUtil;
private final UserRepository userRepository;
private final UserAuthCacheService userAuthCacheService;
private final JwtProperties jwtProperties;

@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
Expand All @@ -54,14 +59,23 @@ protected void doFilterInternal(HttpServletRequest request, HttpServletResponse
jwtUtil.validateTokenType(jwt, TOKEN_TYPE_ACCESS);

Long userId = jwtUtil.getUserIdFromToken(jwt);
User user = userRepository.findById(userId)
.orElseThrow(() -> new GeneralException(AuthErrorCode.USER_NOT_FOUND));
UserPrincipal userPrincipal = userAuthCacheService.get(userId);

if (user.isWithdrawn()) {
if (userPrincipal == null) {
User user = userRepository.findById(userId)
.orElseThrow(() -> new GeneralException(AuthErrorCode.USER_NOT_FOUND));

userPrincipal = UserPrincipal.buildUserPrincipal(user);

if (userPrincipal.getStatus() == UserStatus.WITHDRAWN) {
throw new GeneralException(AuthErrorCode.WITHDRAWN_USER);
}

userAuthCacheService.put(userId, user, jwtProperties.getAccessTokenExpiration());
} else if (userPrincipal.getStatus() == UserStatus.WITHDRAWN) {
throw new GeneralException(AuthErrorCode.WITHDRAWN_USER);
}

UserPrincipal userPrincipal = UserPrincipal.buildUserPrincipal(user);
UsernamePasswordAuthenticationToken authentication = createAuthentication(userPrincipal, request);

SecurityContext securityContext = SecurityContextHolder.createEmptyContext();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
import com.techfork.domain.user.repository.UserRepository;
import com.techfork.global.exception.GeneralException;
import com.techfork.global.security.auth.service.RefreshTokenService;
import com.techfork.global.security.auth.service.UserAuthCacheService;
import com.techfork.global.security.jwt.JwtDTO;
import com.techfork.global.security.jwt.JwtProperties;
import com.techfork.global.security.jwt.JwtUtil;
Expand Down Expand Up @@ -60,6 +61,9 @@ class AuthServiceTest {
@Mock
private KakaoOAuthService kakaoOAuthService;

@Mock
private UserAuthCacheService userAuthCacheService;

@InjectMocks
private AuthService authService;

Expand Down Expand Up @@ -96,6 +100,7 @@ void refreshToken_Success() {
given(userRepository.findById(userId)).willReturn(Optional.of(user));
given(jwtUtil.generateTokens(userId, Role.USER)).willReturn(newTokens);
given(jwtProperties.getRefreshTokenExpiration()).willReturn(900000L);
given(jwtProperties.getAccessTokenExpiration()).willReturn(180000L);

// When
TokenRefreshResponse result = authService.refreshToken(validRefreshToken, response);
Expand All @@ -105,6 +110,7 @@ void refreshToken_Success() {
verify(jwtUtil).isValidToken(validRefreshToken);
verify(jwtUtil).validateTokenType(validRefreshToken, TOKEN_TYPE_REFRESH);
verify(refreshTokenService).saveRefreshToken(eq(userId), eq(newRefreshToken), anyLong());
verify(userAuthCacheService).put(eq(userId), eq(user), eq(180000L));
verify(response).addHeader(eq("Set-Cookie"), anyString());
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
import com.techfork.domain.user.exception.UserErrorCode;
import com.techfork.domain.user.repository.UserRepository;
import com.techfork.global.exception.GeneralException;
import com.techfork.global.security.auth.service.UserAuthCacheService;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
Expand Down Expand Up @@ -40,6 +41,9 @@ class UserCommandServiceTest {
@Mock
private UserRepository userRepository;

@Mock
private UserAuthCacheService userAuthCacheService;

@InjectMocks
private UserCommandService userCommandService;

Expand Down Expand Up @@ -81,6 +85,7 @@ void completeOnboarding_Success() {

verify(userRepository, times(1)).findByIdWithInterestCategories(userId);
verify(interestCommandService, times(1)).saveUserInterests(eq(mockUser), any(SaveInterestRequest.class));
verify(userAuthCacheService).evict(userId);
}

@Test
Expand Down Expand Up @@ -142,6 +147,7 @@ void completeOnboarding_NullDescription_Success() {
assertThat(mockUser.getDescription()).isNull();

verify(interestCommandService, times(1)).saveUserInterests(eq(mockUser), any(SaveInterestRequest.class));
verify(userAuthCacheService).evict(userId);
}

@Test
Expand Down Expand Up @@ -182,6 +188,7 @@ void completeOnboarding_MultipleCategories_Success() {
// Then
assertThat(mockUser.getNickName()).isEqualTo("풀스택개발자");
verify(interestCommandService, times(1)).saveUserInterests(eq(mockUser), any(SaveInterestRequest.class));
verify(userAuthCacheService).evict(userId);
}

// ===== 프로필 수정 테스트 =====
Expand Down Expand Up @@ -308,6 +315,7 @@ void withdrawUser_Success() {
assertThat(testUser.getSocialId()).isEqualTo(originalSocialId);

verify(userRepository).findById(userId);
verify(userAuthCacheService).evict(userId);
}

@Test
Expand Down
Loading