Skip to content

Commit

Permalink
[Feature] 회원탈퇴 구현 (#55)
Browse files Browse the repository at this point in the history
* feat: 아카이브 생성, 수정 유효성 검사 추가 (#32)

* refactor: 불필요한 어노테이션 제거 (#32)

* feat: 회원탈퇴 구현 (#32)
  • Loading branch information
junseoplee authored Oct 4, 2024
1 parent 5369225 commit c139511
Show file tree
Hide file tree
Showing 10 changed files with 113 additions and 63 deletions.
47 changes: 31 additions & 16 deletions src/main/java/project/backend/business/auth/AuthService.java
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import project.backend.business.auth.implement.KakaoLoginManager;
import project.backend.business.user.implement.UserManager;
import project.backend.business.user.implement.UserReader;
import project.backend.entity.token.BlacklistToken;
import project.backend.business.auth.request.TokenServiceRequest;
import project.backend.common.error.CustomException;
Expand All @@ -25,8 +27,10 @@
@RequiredArgsConstructor
public class AuthService {

private final TokenProvider tokenProvider;
private final KakaoLoginManager kakaoLoginManager;
private final UserManager userManager;
private final UserReader userReader;
private final TokenProvider tokenProvider;
private final RefreshTokenRedisRepository refreshTokenRedisRepository;
private final BlacklistTokenRedisRepository blacklistTokenRedisRepository;

Expand All @@ -45,22 +49,14 @@ public TokenServiceResponse kakaoLogin(String code) throws JsonProcessingExcepti

@Transactional
public void logout(TokenServiceRequest tokenServiceRequest) {
String accessToken = tokenServiceRequest.getAccessToken();
String refreshToken = tokenServiceRequest.getRefreshToken();

if (accessToken == null || !tokenProvider.validate(accessToken)) {
throw new CustomException(ErrorCode.INVALID_ACCESS_TOKEN);
}

long expiration = tokenProvider.getExpiration(accessToken);
blacklistTokenRedisRepository.save(BlacklistToken.builder()
.token(accessToken)
.expiration(expiration / 1000)
.build());

refreshTokenRedisRepository.deleteById(refreshToken);
invalidateTokens(tokenServiceRequest);
}

SecurityContextHolder.clearContext();
@Transactional
public void withdraw(Long userId, TokenServiceRequest tokenServiceRequest) {
User user = userReader.readUserById(userId);
userManager.withdrawUser(user);
invalidateTokens(tokenServiceRequest);
}

@Transactional
Expand Down Expand Up @@ -102,6 +98,25 @@ public TokenServiceResponse reissueAccessToken(TokenServiceRequest tokenServiceR
return tokenServiceResponse;
}

private void invalidateTokens(TokenServiceRequest tokenServiceRequest) {
String accessToken = tokenServiceRequest.getAccessToken();
String refreshToken = tokenServiceRequest.getRefreshToken();

if (accessToken == null || !tokenProvider.validate(accessToken)) {
throw new CustomException(ErrorCode.INVALID_ACCESS_TOKEN);
}

long expiration = tokenProvider.getExpiration(accessToken);
blacklistTokenRedisRepository.save(BlacklistToken.builder()
.token(accessToken)
.expiration(expiration / 1000)
.build());

refreshTokenRedisRepository.deleteById(refreshToken);

SecurityContextHolder.clearContext();
}

private void saveRefreshTokenOnRedis(User user, TokenServiceResponse response) {
RefreshToken refreshToken = RefreshToken.builder()
.id(user.getId())
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,7 @@ public User getKakaoUser(String token) throws JsonProcessingException {
String name = kakaoUserInfo.getName();
String profileImageUrl = kakaoUserInfo.getProfileImageUrl();

return userRepository.findByEmail(email)
return userRepository.findByEmailAndActivatedTrue(email)
.orElseGet(() -> userRepository.save(
User.createUser(email, name, profileImageUrl)
));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ public OAuth2User loadUser(OAuth2UserRequest userRequest) {
String name = kakaoUserInfo.getName();
String profileImageUrl = kakaoUserInfo.getProfileImageUrl();

User user = userRepository.findByEmail(email)
User user = userRepository.findByEmailAndActivatedTrue(email)
.orElseGet(
() -> userRepository.save(
User.createUser(email, name, profileImageUrl)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package project.backend.business.user.implement;

import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Component;
import project.backend.entity.user.User;
import project.backend.repository.user.UserRepository;

@Component
@RequiredArgsConstructor
public class UserManager {

private final UserRepository userRepository;

public void withdrawUser(User user) {
user.withdraw();
userRepository.save(user);
}
}
7 changes: 5 additions & 2 deletions src/main/java/project/backend/entity/user/User.java
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@
import jakarta.persistence.Table;
import lombok.AccessLevel;
import lombok.Builder;
import lombok.EqualsAndHashCode;
import lombok.Getter;
import lombok.NoArgsConstructor;
import project.backend.entity.BaseEntity;
Expand All @@ -17,7 +16,6 @@
@Getter
@Table(name = "users")
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@EqualsAndHashCode(of = "id", callSuper = false)
public class User extends BaseEntity {

@Id
Expand Down Expand Up @@ -48,4 +46,9 @@ public static User createUser(String email, String name, String profileImageUrl)
.profileImageUrl(profileImageUrl)
.build();
}

public void withdraw() {
this.setActivated(false);
this.email = null;
}
}
Original file line number Diff line number Diff line change
@@ -1,13 +1,17 @@
package project.backend.presentation.archive.request;

import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.Pattern;
import jakarta.validation.constraints.Size;
import lombok.Getter;
import project.backend.business.archive.request.CreateUpdateArchiveServiceRequest;

@Getter
public class CreateUpdateArchiveRequest {

@NotBlank(message = "아카이브 이름은 필수 입력 값입니다.")
@Size(min = 2, max = 30, message = "아카이브 이름은 2자 이상 30자 이하로 입력해야 합니다.")
@Pattern(regexp = "^[a-zA-Z0-9가-힣 ]*$", message = "아카이브 이름에는 특수 문자를 포함할 수 없습니다.")
private String name;

public CreateUpdateArchiveServiceRequest toServiceRequest() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
import lombok.RequiredArgsConstructor;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
Expand All @@ -17,6 +18,8 @@
import project.backend.presentation.auth.docs.AuthControllerDocs;
import project.backend.presentation.auth.util.TokenCookieManager;
import project.backend.presentation.auth.util.TokenExtractor;
import project.backend.security.aop.AssignCurrentUserInfo;
import project.backend.security.aop.CurrentUserInfo;

@RestController
@RequiredArgsConstructor
Expand All @@ -27,7 +30,7 @@ public class AuthController implements AuthControllerDocs {
private final TokenExtractor tokenExtractor;
private final TokenCookieManager tokenCookieManager;

@RequestMapping("/login/kakao")
@PostMapping("/login/kakao")
public ResponseEntity<TokenServiceResponse> loginKakao(
@RequestParam(name = "code") String code,
HttpServletResponse response) throws JsonProcessingException {
Expand Down Expand Up @@ -63,4 +66,18 @@ public ResponseEntity<Void> logout(

return new ResponseEntity<>(HttpStatus.OK);
}

@AssignCurrentUserInfo
@DeleteMapping("/withdraw")
public ResponseEntity<Void> withdraw(
CurrentUserInfo userInfo,
HttpServletRequest request,
HttpServletResponse response) {
TokenServiceRequest tokenServiceRequest = tokenExtractor.extractTokenRequest(request);
authService.withdraw(userInfo.getUserId(), tokenServiceRequest);

tokenCookieManager.removeRefreshTokenCookie(response);

return new ResponseEntity<>(HttpStatus.OK);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,5 +8,5 @@
@Repository
public interface UserRepository extends JpaRepository<User, Long> {

Optional<User> findByEmail(String email);
Optional<User> findByEmailAndActivatedTrue(String email);
}
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@
@Component
public class AssignCurrentUserInfoAspect {

// @AssignCurrentUserInfo 가 있는 메서드 실행 전에 현재 유저의 ID를 CurrentUserInfo 객체에 할당
// @AssignCurrentUserInfo가 있는 메서드 실행 전에 현재 유저의 ID를 CurrentUserInfo 객체에 할당
@Before("@annotation(project.backend.security.aop.AssignCurrentUserInfo)")
public void assignUserId(JoinPoint joinPoint) {
log.info("Starting assignUserId before method execution.");
Expand All @@ -30,50 +30,40 @@ public void assignUserId(JoinPoint joinPoint) {

if (arg instanceof CurrentUserInfo) {
log.info("Processing argument at index {}: {}", i, arg.getClass().getSimpleName());
getMethod(arg.getClass()).ifPresentOrElse(
setUserId -> {
Long userId = getCurrentUserId();
log.info("Setting userId: {}", userId);
invokeMethod(arg, setUserId, userId);
},
() -> log.warn("No setUserId method found for class: {}",
arg.getClass().getSimpleName())
);
getMethod(arg.getClass()).ifPresentOrElse(setUserId -> {
Long userId = getCurrentUserId();
log.info("Setting userId: {}", userId);
invokeMethod(arg, setUserId, userId);
}, () -> log.warn("No setUserId method found for class: {}", arg.getClass().getSimpleName()));
} else {
log.info("Skipping argument at index {}: Not a CurrentUserInfo instance", i);
}
}
}

// Login 했을 경우 현재 유저의 ID를 CurrentUserInfo 객체에 할당
// Login 하지 않았을 경우 CurrentUserInfo 객체에 null 할당
// 로그인 했을 경우 현재 유저의 ID를 CurrentUserInfo 객체에 할당
// 로그인 하지 않았을 경우 CurrentUserInfo 객체에 null 할당
@Before("@annotation(project.backend.security.aop.AssignOrNullCurrentUserInfo)")
public void assignUserIdOrNull(JoinPoint joinPoint) {
log.info("Starting assignUserIdOrNull before method execution.");
Arrays.stream(joinPoint.getArgs())
.forEach(arg -> {
if (arg == null) {
log.warn("Argument is null, skipping this argument.");
} else {
log.info("Processing argument: {}", arg.getClass().getSimpleName());
getMethod(arg.getClass()).ifPresentOrElse(
setUserId -> {
Long userId = getCurrentUserIdOrNull();
log.info("Setting userId: {}", userId);
invokeMethod(arg, setUserId, userId);
},
() -> log.warn("No setUserId method found for class: {}",
arg.getClass().getSimpleName())
);
}
});
Arrays.stream(joinPoint.getArgs()).forEach(arg -> {
if (arg == null) {
log.warn("Argument is null, skipping this argument.");
} else {
log.info("Processing argument: {}", arg.getClass().getSimpleName());
getMethod(arg.getClass()).ifPresentOrElse(setUserId -> {
Long userId = getCurrentUserIdOrNull();
log.info("Setting userId: {}", userId);
invokeMethod(arg, setUserId, userId);
}, () -> log.warn("No setUserId method found for class: {}", arg.getClass().getSimpleName()));
}
});
}

// arg 객체의 setUserId 메서드를 호출하여 현재 유저의 ID를 할당
private void invokeMethod(Object arg, Method method, Long currentUserId) {
try {
log.info("Invoking method: {} on class: {} with userId: {}", method.getName(),
arg.getClass().getSimpleName(), currentUserId);
log.info("Invoking method: {} on class: {} with userId: {}", method.getName(), arg.getClass().getSimpleName(), currentUserId);
method.invoke(arg, currentUserId);
} catch (ReflectiveOperationException e) {
log.error("Error invoking method: {}", method.getName(), e);
Expand All @@ -94,11 +84,10 @@ private Optional<Method> getMethod(Class<?> clazz) {
// 현재 유저의 ID를 반환
private Long getCurrentUserId() {
log.info("Retrieving current user ID.");
return getCurrentUserIdCheck()
.orElseThrow(() -> {
log.error("No current user ID found.");
return new RuntimeException("User ID not found.");
});
return getCurrentUserIdCheck().orElseThrow(() -> {
log.error("No current user ID found.");
return new RuntimeException("User ID not found.");
});
}

// 현재 유저의 ID가 존재한다면 ID 반환, 없다면 null 반환
Expand All @@ -108,7 +97,7 @@ private Long getCurrentUserIdOrNull() {
}

private Optional<Long> getCurrentUserIdCheck() {
// 현재 SecurityContext 에서 Authentication 객체를 가져옴
// 현재 SecurityContext에서 Authentication 객체를 가져옴
final Authentication authentication = SecurityContextHolder.getContext().getAuthentication();

if (authentication == null || authentication.getPrincipal() == null) {
Expand All @@ -119,19 +108,19 @@ private Optional<Long> getCurrentUserIdCheck() {

Object principal = authentication.getPrincipal();

// principal 이 UserDetails 타입인 경우에만 ID 반환
// principal이 UserDetails 타입인 경우에만 ID 반환
if (principal instanceof KakaoUserDetails kakaoUserDetails) {
log.info("KakaoUserDetails found, user ID: {}", kakaoUserDetails.getId());
return Optional.ofNullable(kakaoUserDetails.getId());
}

// principal 이 UserDetails 가 아닌 경우 예외를 던짐
// principal이 UserDetails가 아닌 경우 예외를 던짐
log.error("Principal is not an instance of KakaoUserDetails.");
throw new CustomException(ErrorCode.NOT_AUTHENTICATED);
}

private Optional<Long> getCurrentUserIdCheckOrNull() {
// 현재 SecurityContext 에서 Authentication 객체를 가져옴
// 현재 SecurityContext에서 Authentication 객체를 가져옴
final Authentication authentication = SecurityContextHolder.getContext().getAuthentication();

if (authentication == null || authentication.getPrincipal() == null) {
Expand All @@ -142,7 +131,7 @@ private Optional<Long> getCurrentUserIdCheckOrNull() {

Object principal = authentication.getPrincipal();

// principal 이 UserDetails 타입인 경우에만 ID 반환
// principal이 UserDetails 타입인 경우에만 ID 반환
if (principal instanceof KakaoUserDetails kakaoUserDetails) {
log.info("KakaoUserDetails found, user ID (or null): {}", kakaoUserDetails.getId());
return Optional.ofNullable(kakaoUserDetails.getId());
Expand Down
4 changes: 4 additions & 0 deletions src/main/java/project/backend/security/jwt/JwtFilter.java
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,10 @@ private static boolean isRequestPassURI(HttpServletRequest request) {
return true;
}

if (request.getRequestURI().equals("/auth/withdraw")) {
return false; // 탈퇴는 필터를 거치도록 설정
}

if (request.getRequestURI().startsWith("/auth")) {
return true;
}
Expand Down

0 comments on commit c139511

Please sign in to comment.