프로젝트/RESTAPI 추천 서비스

유저의 탈퇴, 추방 여부를 추가해보자.

indeep 2024. 5. 20. 22:24

현재 REST API의 서비스는 유저의 추방, 탈퇴 시간과 여부를 기록하지 않는다.

 

즉 회원가입을 진행하면 가입 날짜, 최근 로그인 날짜만 기록하지 탈퇴와 추방에 대한 여부는 고민하지 않았다.

 

추가 이유

 

1. 유저의 탈퇴는 서비스에서 필수적이라고 생각했다. 아무래도 본인의 개인정보를 남기기 싫어하는 유저는 분명 존재할 것이기에 탈퇴 서비스를 추가하려고 한다.

 

2. 유저의 추방은 아무래도 GPT API를 내 개인 API 키로 사용하다보니 요금에 대한 부분을 신경쓰지 않을 수 없었다.

API 요청 기록을 전부 남기기에 기록을 확인해서 비정상적인 유저를 추방시키기 위해 추가하려고 한다.

 

 

Member

@Id @GeneratedValue(strategy = GenerationType.IDENTITY)  
private Long id;                                         
                                                         
@Column(nullable = false, unique = true, length = 50)    
private String email;                                    
                                                         
@Column(nullable = false, length = 100)                  
private String password;                                 
                                                         
@Column(nullable = false, length = 10)                   
private String nickname;                                 
                                                         
@Enumerated(EnumType.STRING)                             
private SocialType socialType;                           
                                                         
@Column(nullable = true)                                
private LocalDateTime loginLastDate;                     
                                                         
@Column(nullable = false)                                
@CreatedDate                                             
private LocalDateTime createDate;                        
                                                         
@Column(nullable = false)                                
private Integer token = 3;                               
                                                         
@OneToMany(fetch = FetchType.LAZY, mappedBy = "member")  
private List<MemberRole> memberRoles = new ArrayList<>();

 

현재 멤버 클래스는 위와 동일하다.

여기에 추방 시간, 탈퇴 시간을 추가하면 된다.

 

@Column(nullable = true)                   
private LocalDateTime bannedDate;          
                                           
@Column(nullable = true)                   
private LocalDateTime withdrawalDate;

 

2개 추가 완료.

얘네는 기존 회원가입하면 null로 존재하다가 탈퇴나, 추방이 되면 값이 채워져 판단을 진행하게 될 것이기에 nullable를 true로 만들었다.

 

 

그럼 해당 컬럼 추가로 기존 로그인 로직이 변경되어야 한다.

 

변경점

 

1. 로그인의 경우 추방 시간, 탈퇴 시간이 있는지 판단해야 한다.

2. 탈퇴만 있는 경우 재회원가입이 가능하지만 추방의 경우 불가능하다.

 

 

MemberServiceImpl

// 로그인                                                                                                                                             
@Override                                                                                                                                          
@Transactional                                                                                                                                     
public void login(LoginInfoRequestDTO loginInfoRequestDTO, HttpServletResponse response) {                                                         
    Member member = memberRepository.findByMemberLogin(loginInfoRequestDTO.getEmail())                                                             
            .orElseThrow(() -> new MemberException(MemberExceptionInfo.FAIL_LOGIN, loginInfoRequestDTO.getEmail() + "에 맞는 유저를 찾지 못했습니다.(로그인 실패)"));
                                                                                                                                                   
    // 추방 여부 판단                                                                                                                                    
    if (member.getBannedDate() != null) {                                                                                                          
        throw new MemberException(MemberExceptionInfo.BANNED_MEMBER, loginInfoRequestDTO.getEmail() + " 유저가 로그인 시도를 진행했습니다.(추방된 유저)");             
    }                                                                                                                                              
                                                                                                                                                   
    // 탈퇴 여부 판단                                                                                                                                    
    if (member.getWithdrawalDate() != null) {                                                                                                      
        throw new MemberException(MemberExceptionInfo.WITHDRAWAL_MEMBER, loginInfoRequestDTO.getEmail() + " 유저가 로그인 시도를 진행했습니다.(탈퇴한 유저)");         
    }

 

서비스에서는 추방 여부, 탈퇴 여는 따로 판단하도록 만들었다. 원래 쿼리 하나로 찾아오려고 했는데 예외 메시지를 별도로 보내고 싶었던 이유가 컸다.

 

 

그런데 소셜로그인의 경우 문제가 발생했다.

 

PrincipalOAuth2UserService

if (byUser.isEmpty()) {                                                                                                                                            
    Member member = new Member(email, nickname, SocialType.KAKAO);                                                                                               
    Member save = memberRepository.save(member);                                                                                                                 
                                                                                                                                                                 
    MemberRole memberRole = MemberRole.builder()                                                                                                                 
            .member(save)                                                                                                                                        
            .build();                                                                                                                                            
    memberRoleRepository.save(memberRole);                                                                                                                       
                                                                                                                                                                                                                                                                           
} else {                                                                                                                                                           
    Member member = byUser.get();                                                                                                                                
                                                                                                                                                                 
    // 추방 여부 판단                                                                                                                                                  
    if (member.getBannedDate() != null) {                                                                                                                          
        throw new MemberException(MemberExceptionInfo.BANNED_USER, member.getEmail() + " 유저가 로그인 시도를 진행했습니다.(추방된 유저)");                                                                                                      
    }                                                                                                                                                            
}

 

위 코드에서 else에서 동일하게 추방 시간이 있으면 예외를 던지도록 했는데 이렇게 하면 failure 핸들러를 타지 않은 상태로 success 핸들러를 타서 문제가 발생했다.

 

그래서 Oauth2AuthenticationExpcetion을 던져줘서 내가 커스텀해놓은 Failure핸들러를 탈 수 있도록 변경해야 한다.

 

Member member = byUser.get();                                                                                    
                                                                                                                 
// 추방 여부 판단                                                                                                      
if (member.getBannedDate() != null) {                                                                            
    OAuth2Error error = new OAuth2Error("추방된 유저", member.getEmail() + " 유저가 로그인 시도를 진행했습니다.(추방된 유저, 카카오)", null);
    throw new OAuth2AuthenticationException(error);                                                              
}                                                                                                                
                                                                                                                 
// 탈퇴 여부 판단                                                                                                      
if (member.getWithdrawalDate() != null) {                                                                        
    OAuth2Error error = new OAuth2Error("탈퇴한 유저", member.getEmail() + " 유저가 로그인 시도를 진행했습니다.(탈퇴한 유저, 카카오)", null);
    throw new OAuth2AuthenticationException(error);                                                              
}

 

위 코드처럼 예외를 생성해서 던져주면 

 

FailureHandler

@Slf4j
@Component
public class FailureHandler implements AuthenticationFailureHandler {

    private final RedirectStrategy redirectStrategy = new DefaultRedirectStrategy();

    @Override
    public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException, ServletException {
        log.error("oauth 실패 핸들러 : " + exception.getMessage());

        // url 생성
        String url = makeRedirectUrl();

        redirectStrategy.sendRedirect(request, response, url);
    }

    // 리다이렉트 주소
    private String makeRedirectUrl() {
        return UriComponentsBuilder.fromUriString("http://localhost:5173/failure")
                .encode(StandardCharsets.UTF_8)
                .build().toUriString();
    }
}

 

얘를 타도록 만들어서 프론트에서 메시지를 띄우도록 만들었다.

 

이런 느낌

 

탈퇴 API 추가

 

해당 API를 호출하면 유저는 계정을 탈퇴하게 된다. 

원래 탈퇴 API를 호출하면 바로 탈퇴를 진행시킬까 했는데 안전하게 비밀번호 확인을 한 번더 진행해서 통과되면 탈퇴시키도록 만들어보자.

 

아...경로 이름을 어떻게 짓지!!!

내 서비스에서 제공해주는 1번을 사용해보자.

그런데 나는 쿠키로 유저의 JWT를 매번 제공하고 있어서 굳이 패스 경로에 {id}를 제공하지 않아도 된다.

그러니 /users/deactivate 로 사용하면 될 것 같다.

 

그리고 비밀번호를 받아야하니 requestDTO를 하나 만들어야 한다.

 

근데 또 만들자니 한 가지 문제점이 발생했는데...

내 서비스는 일반 유저, 소셜 유저가 존재한다. 일반 유저의 경우 비밀번호를 암호화시켜 진행했기에 요청으로 비밀번호를 받는데 소셜 유저의 경우 비밀번호를 입력할 필요가 없다. 그러면 일단 컨트롤러에서 받는 경우가 달라지니 결국 각각의 api를 따로 구분시켜야하나? 

 

그래서 하나의 엔드포인트에서 처리를 진행하는데 switch를 통해 분기점을 나누도록 결정했다. 즉 컨트롤러는 하나로 받지만 서비스는 따로 나눠질 수 있도록

 

 

MemberController

// 유저 탈퇴                                                                                                        
@PatchMapping("members/deactivate")                                                                             
public ResponseEntity<ApiResponse<Void>> deactivateMember(@RequestBody @Valid DeactivateRequestDTO requestDTO) {
    SocialType socialType = requestDTO.getSocialType();                                                         
                                                                                                                
    switch (socialType) {                                                                                       
        case KAKAO -> memberService.deactivateSocialMember();                                                   
        case GENERAL -> memberService.deactivateGeneralMember(requestDTO);                                      
        default -> throw new IllegalArgumentException("잘못된 타입");                                                
    }                                                                                                           
                                                                                                                
    return ResponseEntity.status(HttpStatus.OK).body(ApiResponse.createSuccessNoContent("회원 탈퇴에 성공했습니다."));     
}

 

 

MemberServiceImpl

// 일반 유저 탈퇴                                                                                                                    
@Override                                                                                                                      
@Transactional                                                                                                                 
public void deactivateGeneralMember(DeactivateRequestDTO requestDTO) {                                                         
    Member currentMember = getCurrentMember();                                                                                 
    if (!encoder.matches(requestDTO.getPassword(), currentMember.getPassword())) {                                             
        throw new MemberException(MemberExceptionInfo.NOT_MATCH_PASSWORD, currentMember.getEmail() + " 유저 비밀번호 불일치 발생(회원 탈퇴)");
    }                                                                                                                          
                                                                                                                               
    currentMember.updateWithdrawalDate();                                                                                      
}                                                                                                                              
                                                                                                                               
// 소셜 유저 탈퇴                                                                                                                    
@Override                                                                                                                      
@Transactional                                                                                                                 
public void deactivateSocialMember() {                                                                                         
    Member currentMember = getCurrentMember();                                                                                 
                                                                                                                               
    currentMember.updateWithdrawalDate();                                                                                      
}

 

이렇게 로직을 작성했다.

 

1. 이미 JWT를 통해 유저 정보를 가져온다.

2. 일반 유저의 경우 요청 비밀번호랑 현재 비밀번호 일치 여부 판단(암호화로 진행)

3. 소셜 유저의 경우 판단 없이 바로 탈퇴 진행

4. 탈퇴 시간 추가.

 

일반 유저는 사실 비밀번호 판단을 진행해서 한번 더 보안(?)을 챙긴다고 생각하는데 소셜 유저의 경우 뭔가 허점이 많아보이는데...

 

일단 테스트 코드도 통과했으니 이제 프론트만 만들면 된다...으어ㅓ

 

 

ㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡ

 

추방 API 추가

 

이번에도 RESTAPI 서비스의 힘을 빌려보자.

 

 

/admin/members/{id}/ban 해당 경로가 제일 나을 것 같다.

 

MemberController

// 유저 추방                                                                                                   
@PatchMapping("admin/members/{id}/ban")                                                                    
public ResponseEntity<ApiResponse<?>> bannedMember(@PathVariable(name = "id") Long id) {                   
    memberService.bannedMember(id);                                                                        
                                                                                                           
    return ResponseEntity.status(HttpStatus.OK).body(ApiResponse.createSuccessNoContent("회원 추방에 성공했습니다."));
}

 

컨트롤러는 경로로 유저의 id를 받아서 넘기도록 구현.

 

MemberServiceImpl

// 유저 추방                                                                                                                  
@Override                       
@Transactional
public void bannedMember(Long id) {                                                                                       
    Member member = memberRepository.findById(id)                                                                         
            .orElseThrow(() -> new MemberException(MemberExceptionInfo.NOT_FOUND_MEMBER, id + "를 가진 유저가 존재하지 않습니다.(추방)"));
                                                                                                                          
    // 추방 시간 추가                                                                                                           
    member.updateBannedDate();                                                                                            
}

 

id로 멤버를 찾았다. 원래 기존에 추방 시간이 null이 아니면 예외를 던질까 했는데, 사실 추방기록이 있는 유저를 다시 추방한다고 해서 문제가 발생하진 않는다고 판단. 그래서 로직을 간소화했다.

 

 

그래..너라도 잘 돌아가니 다행이네..

반응형