회원 가입, 수정, 탈퇴 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
복사
테스트