Blog

[Spring][iLanD] MVP - [Users] 회원 CUD API

Category
Author
Tags
PinOnMain
1 more property
회원 가입, 수정, 탈퇴 API 구현
비밀번호는 SpringSecurity의 BCryptPasswordEncoder를 통해 기본 단방향 해시 알고리즘 BCrypt 로 복호화 불가능한 암호화 기능을 활용
PasswordEncoder.java config패키지에 별도로 작성하여 정의했으며 필요 클래스(UserService)에 주입되고 사용됨
BCrypt는 안전한 해시 함수로 알려져 있으며, 해시된 값과 함께 사용되는 솔트(salt)를 통해 보안성을 강화
기본적으로 입력값의 유효성 검사는 다음과 같음
회원의 로그인 아이디가 될 e-mail은 UK로 중복이 불가능, @Email 어노테이션에 따른 이메일 유효성 검사
ex)abc@abc.com
이름은 한글 2~4글자까지 허용
ex)김테스, 김테, 김테스트 OK
비밀번호는 비밀번호는 8~15자 영문 대 소문자, 숫자, 특수문자를 포함해야 함
ex)test1234!
usertype은 프론트에서 드롭다운박스로 value를 0,1,2 를 전달하도록 설계할 예정
0인 경우 USER
1인 경우 ADMIN 회원 가입이 가능하지만, token 필드를 잘못 입력하면 가입 불가, “admin”을 정확히 입력해야 admin 회원 가입 완료
2인 경우 STAFF 회원 가입이 가능하지만, token 필드를 잘못 입력하면 가입 불가, “staff”을 정확히 입력해야 staff 회원 가입 완료
UserSignupRequestDto.java
@NoArgsConstructor @AllArgsConstructor @Getter @Setter @Builder public class UserSignupRequestDto { @Email @NotBlank private String useremail; //@Pattern(regexp = "(?=.*[0-9])(?=.*[a-zA-Z]).{4,10}", message = "아이디 4~10자 영문 소문자, 숫자를 사용하세요.") @Pattern(regexp = "^[가-힣]{2,4}$", message = "이름은 한글로 2글자에서 4글자까지 입력하세요.") private String username; @Pattern(regexp = "^(?=.*[a-zA-Z])((?=.*\\d)|(?=.*\\W)).{8,15}+$", message = "비밀번호는 8~15자 영문 대 소문자, 숫자를 사용하세요.") private String password1; private String password2; @Builder.Default private long usertype = 0; @Builder.Default private String token = ""; }
Java
복사
UserUpdateRequestDto.java
@Data @Builder public class UserUpdateRequestDto { @Pattern(regexp = "^(?=.*[a-zA-Z])((?=.*\\d)|(?=.*\\W)).{8,15}+$", message = "비밀번호는 8~15자 영문 대 소문자, 숫자를 사용하세요.") private String password1; private String password2; }
Java
복사
UserController.java
@RestController @RequestMapping("/api/users") @RequiredArgsConstructor public class UserController { private final UserService userService; // 회원가입 @PostMapping("/signup") public StatusResponseDto signup(@Valid @RequestBody UserSignupRequestDto requestDto){ return userService.signup(requestDto); } // 회원탈퇴 @DeleteMapping("/escape") public StatusResponseDto escape(@AuthenticationPrincipal UserDetailsImpl userDetails, @Valid @RequestBody UserDeleteRequestDto requestDto) { if (userDetails == null) { return new StatusResponseDto("로그인이 필요합니다.", HttpStatus.UNAUTHORIZED.value()); } return userService.escape(userDetails.getUser(), requestDto); } // 회원수정 @PutMapping("/update") public StatusResponseDto update(@AuthenticationPrincipal UserDetailsImpl userDetails, @Valid @RequestBody UserUpdateRequestDto requestDto){ if (userDetails == null) { return new StatusResponseDto("로그인이 필요합니다.", HttpStatus.UNAUTHORIZED.value()); } return userService.update(userDetails.getUser(), requestDto); } }
Java
복사
UserService.java
@Service @RequiredArgsConstructor public class UserService { private final UserRepository userRepository; private final PasswordEncoder passwordEncoder; private final String ADMIN_TOKEN = "admin"; private final String STAFF_TOKEN = "staff"; // 회원가입 public StatusResponseDto signup(UserSignupRequestDto requestDto) { try { String email = requestDto.getEmail(); String username = requestDto.getUsername(); String password = passwordEncoder.encode(requestDto.getPassword1()); passwordCheck(requestDto); emailCheck(email); UserRoleEnum role = getUserRoleEnum(requestDto); // 사용자 등록 User user = new User(email, username, password, role); userRepository.save(user); // 메시지와 상태 코드를 전달하여 StatusResponseDto 생성 return new StatusResponseDto("회원가입이 완료되었습니다.", HttpStatus.OK.value()); } catch (DuplicateEmailException e) { // 중복된 이메일 예외 처리 return new StatusResponseDto(e.getMessage(), HttpStatus.BAD_REQUEST.value()); } catch (IllegalArgumentException e) { // 기타 예외 처리 return new StatusResponseDto(e.getMessage(), HttpStatus.INTERNAL_SERVER_ERROR.value()); } } // 회원탈퇴 @Transactional public StatusResponseDto escape(User user, UserDeleteRequestDto requestDto) { if (user == null) { return new StatusResponseDto("로그인이 필요합니다.", HttpStatus.UNAUTHORIZED.value()); } String email = user.getEmail(); if (!checkUserPassword(email, requestDto.getPassword1())) { // 비밀번호가 일치하지 않으면 에러 응답 return new StatusResponseDto("비밀번호가 일치하지 않습니다.", HttpStatus.UNAUTHORIZED.value()); } // 비밀번호가 일치하면 사용자 삭제 deleteUser(email); // 쿠키 삭제 ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes(); HttpServletResponse response = attributes.getResponse(); Cookie cookie = new Cookie("Authorization", null); cookie.setMaxAge(0); cookie.setPath("/"); response.addCookie(cookie); // 회원탈퇴 완료 메시지 응답 return new StatusResponseDto("회원탈퇴가 완료되었습니다.", HttpStatus.OK.value()); } // 회원수정 @Transactional public StatusResponseDto update(User user, UserUpdateRequestDto requestDto) { if (user == null) { return new StatusResponseDto("로그인이 필요합니다.", HttpStatus.UNAUTHORIZED.value()); } if (!requestDto.getPassword1().equals(requestDto.getPassword2())) { return new StatusResponseDto("비밀번호가 일치하지 않습니다.", HttpStatus.BAD_REQUEST.value()); } User updateUser = userRepository.findByEmail(user.getEmail()).orElseThrow( () -> new IllegalArgumentException("해당 사용자가 존재하지 않습니다.") ); String passwordE = passwordEncoder.encode(requestDto.getPassword1()); updateUser.update(passwordE); // 메시지와 상태 코드를 전달하여 StatusResponseDto 생성 return new StatusResponseDto("비밀번호가 변경되었습니다.", HttpStatus.OK.value()); } // 모듈화 메서드 @Transactional public void deleteUser(String email) { User user = userRepository.findByEmail(email).orElseThrow( ()->new IllegalArgumentException("해당 사용자가 존재하지 않습니다.") ); userRepository.delete(user); } public void passwordCheck(UserSignupRequestDto requestDto) { // 비밀번호1, 2 확인 if(!requestDto.getPassword1().equals(requestDto.getPassword2())){ throw new IllegalArgumentException("비밀번호가 일치하지 않습니다."); } } public void emailCheck(String email) { // email 중복 확인 Optional<User> checkEmail = userRepository.findByEmail(email); if(checkEmail.isPresent()){ throw new DuplicateEmailException("중복된 email 입니다."); } } public UserRoleEnum getUserRoleEnum(UserSignupRequestDto requestDto) { // 사용자 ROLE 확인 (미입력시 default User) UserRoleEnum role = UserRoleEnum.USER; long userType = requestDto.getUsertype(); String token = requestDto.getToken(); if (userType == 1) { validateToken(token, ADMIN_TOKEN, 1); role = UserRoleEnum.ADMIN; } else if (userType == 2) { validateToken(token, STAFF_TOKEN, 2); role = UserRoleEnum.STAFF; } return role; } private void validateToken(String inputToken, String expectedToken, long userType) { if (userType == 0) { // usertype이 0인 경우에는 토큰이 없어도 정상 처리 return; } if (inputToken == null || inputToken.isEmpty()) { throw new IllegalArgumentException("토큰을 입력하세요."); } String userTypeString = (userType == 1) ? "ADMIN" : "STAFF"; if (!expectedToken.equals(inputToken)) { throw new IllegalArgumentException(userTypeString + " 토큰이 유효하지 않습니다."); } } private boolean checkUserPassword(String email, String password) { // 이메일로 사용자 찾기 User user = userRepository.findByEmail(email).orElse(null); // 사용자가 존재하고 비밀번호가 일치하면 true 반환 return user != null && passwordEncoder.matches(password, user.getPassword()); } }
Java
복사
UserRepository.java
public interface UserRepository extends JpaRepository<User, Long> { Optional<User> findByUseremail(String useremail); }
Java
복사
테스트