Blog

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

Author
Summary
Category
Project
Tags
Spring
Favorite
Memory Date
2023/11/19 → 2023/11/20
Progress Status
Done
Cross Reference Study
Related Media
Related Thought
Related Lessons
tag
날짜
2023/11/19 → 2023/11/20
작성자
진행상황
완료
태그구분
개발
백엔드
5 more properties
📌
회원 가입, 수정, 탈퇴 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
복사
📌
테스트