Skip to content

Commit

Permalink
Merge pull request #153 from TrainingDiary/enhancement/redis-fix-user…
Browse files Browse the repository at this point in the history
…-cache

Redis 토큰 삭제 버그 수정 및 User Cache 도입 기능 형상
  • Loading branch information
kyoo0115 authored Jul 26, 2024
2 parents 2e3b549 + b552dbc commit 804a240
Show file tree
Hide file tree
Showing 7 changed files with 95 additions and 22 deletions.
3 changes: 3 additions & 0 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,9 @@ dependencies {
// FFmpeg
implementation 'net.bramp.ffmpeg:ffmpeg:0.8.0'

// Caffeine
implementation 'com.github.ben-manes.caffeine:caffeine:3.1.6'

// Querydsl
implementation 'com.querydsl:querydsl-jpa:5.0.0:jakarta'
annotationProcessor "com.querydsl:querydsl-apt:${dependencyManagement.importedProperties['querydsl.version']}:jakarta"
Expand Down
20 changes: 20 additions & 0 deletions src/main/java/com/project/trainingdiary/config/CacheConfig.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package com.project.trainingdiary.config;

import com.github.benmanes.caffeine.cache.Cache;
import com.github.benmanes.caffeine.cache.Caffeine;
import com.project.trainingdiary.model.UserPrincipal;
import java.util.concurrent.TimeUnit;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class CacheConfig {

@Bean
public Cache<String, UserPrincipal> userCache() {
return Caffeine.newBuilder()
.expireAfterWrite(10, TimeUnit.MINUTES)
.maximumSize(1000)
.build();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,6 @@ public class EditTraineeInfoRequestDto {
private double height;

@Schema(example = "5")
@Positive(message = "remainingSession 값은 양수이어야 합니다.")
private int remainingSession;

@Schema(example = "TARGET_WEIGHT", allowableValues = {"TARGET_WEIGHT",
Expand Down
29 changes: 26 additions & 3 deletions src/main/java/com/project/trainingdiary/model/UserPrincipal.java
Original file line number Diff line number Diff line change
Expand Up @@ -2,33 +2,56 @@

import com.project.trainingdiary.entity.TraineeEntity;
import com.project.trainingdiary.entity.TrainerEntity;
import com.project.trainingdiary.model.type.UserRoleType;
import java.util.Collection;
import java.util.Collections;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.Setter;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;

@Getter
@Setter
@AllArgsConstructor
public class UserPrincipal implements UserDetails {

private final Long id;
private final String email;
private final String password;
private final String name;
private final UserRoleType role;
private final Collection<? extends GrantedAuthority> authorities;
private final boolean isTrainer;
private final TrainerEntity trainer;
private final TraineeEntity trainee;

public static UserPrincipal create(TraineeEntity trainee) {
return new UserPrincipal(
trainee.getId(),
trainee.getEmail(),
trainee.getPassword(),
Collections.singletonList(new SimpleGrantedAuthority("ROLE_TRAINEE"))
trainee.getName(),
UserRoleType.TRAINEE,
Collections.singletonList(new SimpleGrantedAuthority("ROLE_TRAINEE")),
false,
null,
trainee
);
}

public static UserPrincipal create(TrainerEntity trainer) {
return new UserPrincipal(
trainer.getId(),
trainer.getEmail(),
trainer.getPassword(),
Collections.singletonList(new SimpleGrantedAuthority("ROLE_TRAINER"))
trainer.getName(),
UserRoleType.TRAINER,
Collections.singletonList(new SimpleGrantedAuthority("ROLE_TRAINER")),
true,
trainer,
null
);
}

Expand Down Expand Up @@ -66,4 +89,4 @@ public boolean isCredentialsNonExpired() {
public boolean isEnabled() {
return true;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,10 @@ private void handleRefreshToken(HttpServletRequest request, HttpServletResponse
UserDetails userDetails = userService.loadUserByUsername(username);

if (userDetails != null) {
String previousAccessTokenKey = "accessToken:" + username;

redisTokenRepository.deleteToken(previousAccessTokenKey);

String newAccessToken = tokenProvider.createAccessToken(username);
log.info("새로운 접근 토큰을 쿠키에 설정: {}", newAccessToken);

Expand Down
55 changes: 37 additions & 18 deletions src/main/java/com/project/trainingdiary/service/UserService.java
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package com.project.trainingdiary.service;

import com.github.benmanes.caffeine.cache.Cache;
import com.project.trainingdiary.dto.request.user.SendVerificationAndCheckDuplicateRequestDto;
import com.project.trainingdiary.dto.request.user.SignInRequestDto;
import com.project.trainingdiary.dto.request.user.SignUpRequestDto;
Expand Down Expand Up @@ -62,6 +63,8 @@ public class UserService implements UserDetailsService {

private final PasswordEncoder passwordEncoder;

private final Cache<String, UserPrincipal> userCache;

private static final String REFRESH_TOKEN_COOKIE_NAME = "Refresh-Token";
private static final String ACCESS_TOKEN_COOKIE_NAME = "Access-Token";

Expand Down Expand Up @@ -98,32 +101,30 @@ public void checkVerificationCode(VerifyCodeRequestDto dto) {
@Transactional
public SignUpResponseDto signUp(SignUpRequestDto dto, HttpServletRequest request,
HttpServletResponse response) {
// 1단계: 회원가입 요청을 검증하고 처리합니다.
VerificationEntity verificationEntity = getVerificationEntity(dto.getEmail());
validateEmailNotExists(dto.getEmail());
validatePasswordsMatch(dto.getPassword(), dto.getConfirmPassword());
validateIfVerified(verificationEntity);

// 2단계: 비밀번호를 인코딩하고 사용자를 저장합니다.
String encodedPassword = passwordEncoder.encode(dto.getPassword());
saveUser(dto, encodedPassword);

UserDetails userDetails = loadUserByUsername(dto.getEmail());
boolean isTrainer = userDetails.getAuthorities().stream()
.anyMatch(authority -> authority.getAuthority().equals("ROLE_TRAINER"));
// 3단계: 데이터베이스에서 사용자 정보를 로드합니다.
UserPrincipal userPrincipal = (UserPrincipal) loadUserByUsername(dto.getEmail());

// 4단계: 더 이상 필요하지 않은 인증 엔티티를 삭제합니다.
verificationRepository.deleteByEmail(dto.getEmail());

generateTokensAndSetCookies(userDetails.getUsername(), request, response);

if (isTrainer) {
TrainerEntity trainer = trainerRepository.findByEmail(dto.getEmail())
.orElseThrow(TrainerNotFoundException::new);
// 5단계: 토큰을 생성하고 쿠키에 설정합니다.
generateTokensAndSetCookies(userPrincipal.getUsername(), request, response);

return SignUpResponseDto.fromEntity(trainer);
// 6단계: 사용자 역할에 따라 적절한 SignUpResponseDto를 반환합니다.
if (userPrincipal.isTrainer()) {
return SignUpResponseDto.fromEntity(userPrincipal.getTrainer());
} else {
TraineeEntity trainee = traineeRepository.findByEmail(dto.getEmail())
.orElseThrow(TraineeNotFoundException::new);

return SignUpResponseDto.fromEntity(trainee);
return SignUpResponseDto.fromEntity(userPrincipal.getTrainee());
}
}

Expand Down Expand Up @@ -388,7 +389,13 @@ private void blacklistToken(Cookie tokenCookie) {
if (tokenCookie != null && tokenProvider.validateToken(tokenCookie.getValue())) {
log.info("블랙리스트에 추가된 토큰: {}", tokenCookie.getValue());
tokenProvider.blacklistToken(tokenCookie.getValue());
redisTokenRepository.deleteToken(tokenProvider.getUsernameFromToken(tokenCookie.getValue()));

String username = tokenProvider.getUsernameFromToken(tokenCookie.getValue());
String accessTokenKey = "accessToken:" + username;
String refreshTokenKey = "refreshToken:" + username;

redisTokenRepository.deleteToken(accessTokenKey);
redisTokenRepository.deleteToken(refreshTokenKey);
}
}

Expand All @@ -406,10 +413,21 @@ public boolean isLocalRequest(HttpServletRequest request) {
*/
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
return traineeRepository.findByEmail(username)
UserPrincipal cachedUser = userCache.getIfPresent(username);
if (cachedUser != null) {
return cachedUser;
}

UserPrincipal userPrincipal = traineeRepository.findByEmail(username)
.map(UserPrincipal::create)
.orElseGet(() -> UserPrincipal.create(trainerRepository.findByEmail(username)
.orElseThrow(() -> new UsernameNotFoundException("존재하지 않는 이메일 입니다."))));
.orElseGet(() -> {
TrainerEntity trainer = trainerRepository.findByEmail(username)
.orElseThrow(() -> new UsernameNotFoundException("존재하지 않는 이메일 입니다."));
return UserPrincipal.create(trainer);
});

userCache.put(username, userPrincipal);
return userPrincipal;
}

/**
Expand All @@ -425,6 +443,7 @@ public MemberInfoResponseDto memberInfo() {
if (authentication == null || authentication.getName() == null) {
throw new UserNotFoundException();
}

String role = authentication.getAuthorities().toString();

if (role.contains("ROLE_TRAINER")) {
Expand All @@ -446,6 +465,6 @@ public MemberInfoResponseDto memberInfo() {
.role(trainee.getRole())
.unreadNotification(trainee.isUnreadNotification())
.build())
.orElseThrow(TrainerNotFoundException::new);
.orElseThrow(TraineeNotFoundException::new);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;

import com.github.benmanes.caffeine.cache.Cache;
import com.project.trainingdiary.dto.request.user.SendVerificationAndCheckDuplicateRequestDto;
import com.project.trainingdiary.dto.request.user.SignInRequestDto;
import com.project.trainingdiary.dto.request.user.SignUpRequestDto;
Expand All @@ -29,6 +30,7 @@
import com.project.trainingdiary.exception.user.VerificationCodeNotMatchedException;
import com.project.trainingdiary.exception.user.VerificationCodeNotYetVerifiedException;
import com.project.trainingdiary.exception.user.WrongPasswordException;
import com.project.trainingdiary.model.UserPrincipal;
import com.project.trainingdiary.model.type.UserRoleType;
import com.project.trainingdiary.provider.CookieProvider;
import com.project.trainingdiary.provider.EmailProvider;
Expand Down Expand Up @@ -83,6 +85,9 @@ public class UserServiceTest {
@Mock
private PasswordEncoder passwordEncoder;

@Mock
private Cache<String, UserPrincipal> userCache;

@InjectMocks
private UserService userService;

Expand Down

0 comments on commit 804a240

Please sign in to comment.