오늘부터 팀 프로젝트 시작이다. 매 프로젝트마다 리더를 뽑아야되는데 , 거의 대부분의 사람들은 핀볼로 정한다
나는 뭔가 사다리타기나 핀볼로 해서 원하지 않은 사람이 리더를 할 방법보단 먼저 여쭤보고 할 사람이 없다면
내가 자처해서한다 로그인/회원가입 구현쪽 부분과 프로필 부분을 맡았다. 일단 시큐리티 적용하고 간단하게
프로젝트 환경변수등 세팅을 했다.
일단 로그인/회원가입 구현했다.
Controller이다.
@RestController
@RequiredArgsConstructor
@RequestMapping("/users")
public class UserController {
private final UserService userService;
@PostMapping("/signup")
public ResponseEntity<StatusCommonResponse> signup(@Valid @RequestBody SignupRequestDto requestDto) {
userService.signup(requestDto);
StatusCommonResponse commonResponse = new StatusCommonResponse(201, "회원가입 성공");
return new ResponseEntity<>(commonResponse, HttpStatus.CREATED);
}
@PostMapping("/logout")
public ResponseEntity<StatusCommonResponse> logout() {
userService.logout();
StatusCommonResponse commonResponse = new StatusCommonResponse(200, "로그아웃 성공");
return new ResponseEntity<>(commonResponse, HttpStatus.OK);
}
@PutMapping("/withdrawal")
public ResponseEntity<StatusCommonResponse> withdrawal(@Valid @RequestBody PasswordRequestDto requestDto) {
userService.withdrawal(requestDto);
StatusCommonResponse commonResponse = new StatusCommonResponse(200, "회원탈퇴 성공");
return new ResponseEntity<>(commonResponse, HttpStatus.OK);
}
@PostMapping("/refresh")
public ResponseEntity<StatusCommonResponse> refresh(HttpServletRequest request) {
HttpHeaders headers = userService.refresh(request);
StatusCommonResponse commonResponse = new StatusCommonResponse(200, "RefreshToken 인증 성공");
return new ResponseEntity<>(commonResponse, headers, HttpStatus.OK);
}
}
Service이다.
@Service
public class UserService {
private final UserRepository userRepository;
private final PasswordEncoder passwordEncoder;
private final JwtUtil jwtUtil;
@Value("${admin.token}")
private String ADMIN_TOKEN;
public UserService(UserRepository userRepository, PasswordEncoder passwordEncoder, JwtUtil jwtUtil) {
this.userRepository = userRepository;
this.passwordEncoder = passwordEncoder;
this.jwtUtil = jwtUtil;
}
@Transactional
public void signup(SignupRequestDto requestDto) {
findByUserId(requestDto.getUserId()).ifPresent((el) -> {
throw new ConflictException("이미 사용 중인 아이디입니다.");
});
UserAuth userAuth = UserAuth.USER;
if (!requestDto.getAdminToken().isEmpty()) {
if (!ADMIN_TOKEN.equals(requestDto.getAdminToken())) {
throw new IllegalArgumentException("관리자 암호가 틀려 등록이 불가능합니다.");
}
userAuth = UserAuth.ADMIN;
}
UserStatus userStatus = UserStatus.ACTIVE;
User user = new User(requestDto, userStatus, userAuth);
String encodedPassword = passwordEncoder.encode(requestDto.getPassword());
user.encryptionPassword(encodedPassword);
userRepository.save(user);
}
@Transactional
public void logout() {
UserDetailsImpl userDetails = (UserDetailsImpl) SecurityContextHolder.getContext().getAuthentication().getPrincipal();
User user = findByUserId(userDetails.getUsername()).orElseThrow(() -> new DataNotFoundException("해당 회원은 존재하지 않습니다."));
user.updateRefreshToken(null);
}
@Transactional
public void withdrawal(PasswordRequestDto requestDto) {
UserDetailsImpl userDetails = (UserDetailsImpl) SecurityContextHolder.getContext().getAuthentication().getPrincipal();
User user = findByUserId(userDetails.getUsername()).orElseThrow(() -> new DataNotFoundException("해당 회원은 존재하지 않습니다."));
if (!checkPassword(requestDto.getPassword(), user.getPassword())) {
throw new BadRequestException("비밀번호를 확인해주세요.");
}
if (user.getUserStatus() == UserStatus.WITHDRAWN) {
throw new ConflictException("이미 탈퇴한 회원입니다.");
}
UserStatus userStatus = UserStatus.WITHDRAWN;
user.updateUserStatus(userStatus);
}
@Transactional
public HttpHeaders refresh(HttpServletRequest request) {
Cookie[] cookies = request.getCookies();
String tokenValue = null;
if (cookies == null) {
throw new BadRequestException("잘못된 요청입니다.");
}
for (Cookie cookie : cookies) {
if ("refreshToken".equals(cookie.getName())) {
tokenValue = cookie.getValue();
}
}
if (!StringUtils.hasText(tokenValue)) {
throw new BadRequestException("잘못된 요청입니다.");
}
jwtUtil.checkTokenExpiration(tokenValue);
if (!jwtUtil.validateToken(tokenValue)) {
throw new UnauthorizedException("토큰 검증 실패");
}
Claims info = jwtUtil.getClaimsFromToken(tokenValue);
User user = findByUserId(info.getSubject()).orElseThrow(() -> new DataNotFoundException("해당 회원은 존재하지 않습니다."));
if (!user.getRefreshToken().equals(tokenValue)) {
throw new UnauthorizedException("토큰 검증 실패");
}
String accessToken = jwtUtil.generateAccessToken(user.getUserId(), user.getUserName(), user.getUserAuth());
String refreshToken = jwtUtil.generateNewRefreshToken(user.getUserId(), user.getUserName(), user.getUserAuth(), info.getExpiration());
ResponseCookie responseCookie = jwtUtil.generateNewRefreshTokenCookie(refreshToken, info.getExpiration());
user.updateRefreshToken(refreshToken);
HttpHeaders headers = new HttpHeaders();
headers.set("Authorization", "Bearer " + accessToken);
headers.add(HttpHeaders.SET_COOKIE, responseCookie.toString());
return headers;
}
public Optional<User> findByUserId(String userId) {
return userRepository.findByUserId(userId);
}
public boolean checkPassword(String requestPassword, String userPassword) {
return passwordEncoder.matches(requestPassword, userPassword);
}
}
Repository이다.
public interface UserRepository extends JpaRepository<User, Long> {
Optional<User> findByUserId(String userId);
Optional<User> findByEmail(String email);
}
Entity이다.
/**
* 사용자 엔티티 클래스 데이터베이스의 사용자 정보를 나타내는 클래스
*/
@Entity
@Getter
@NoArgsConstructor
@Table(name = "users")
public class User extends TimeStamp {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(nullable = false, unique = true, length = 50)
private String userId;
@Column(nullable = false)
private String password;
@Column(nullable = false, length = 50)
private String userName;
@Column(nullable = false, unique = true, length = 50)
private String email;
@Column(length = 100)
private String intro;
@Column(nullable = false)
@Enumerated(EnumType.STRING)
private UserAuth userAuth;
@Column(nullable = false)
@Enumerated(EnumType.STRING)
private UserStatus userStatus;
@Column(unique = true)
private String refreshToken;
@OneToMany(mappedBy = "user", cascade = CascadeType.REMOVE)
private List<Menu> menu = new ArrayList<>();
@OneToMany(mappedBy = "user", cascade = CascadeType.REMOVE)
private List<Review> comments = new ArrayList<>();
private Long kakaoId;
@ElementCollection(fetch = FetchType.EAGER) // FetchType 설정 추가
@CollectionTable(name = "past_passwords", joinColumns = @JoinColumn(name = "user_id"))
@Column(name = "password")
private List<String> pastPasswords = new ArrayList<>();
public User(SignupRequestDto requestDto, UserStatus userStatus, UserAuth userAuth) {
this.userId = requestDto.getUserId();
this.password = requestDto.getPassword();
this.userName = requestDto.getUserName();
this.email = requestDto.getEmail();
this.intro = requestDto.getIntro();
this.userStatus = userStatus;
this.userAuth = userAuth;
this.pastPasswords.add(this.password);
}
public User(String userId, String password, String userName, String email, UserStatus userStatus, UserAuth userAuth, Long kakaoId) {
this.userId = userId;
this.password = password;
this.userName = userName;
this.email = email;
this.userStatus = userStatus;
this.userAuth = userAuth;
this.kakaoId = kakaoId;
this.pastPasswords.add(this.password);
}
public void updateUserStatus(UserStatus userStatus) {
this.userStatus = userStatus;
}
public void updateRefreshToken(String refreshToken) {
this.refreshToken = refreshToken;
}
public void updatePassword(String newPassword) {
if (this.pastPasswords.size() >= 3) {
this.pastPasswords.remove(0);
}
this.pastPasswords.add(newPassword);
this.password = newPassword;
}
public void encryptionPassword(String password) {
this.password = password;
}
public void updateUserName(String userName) {
this.userName = userName;
}
public void updateIntro(String intro) {
this.intro = intro;
}
public void updateProfile(AdminUserProfileRequestDto requestDto) {
this.userName = requestDto.getUserName();
this.intro = requestDto.getIntro();
}
public void updateAuth(UserAuth userAuth) {
this.userAuth = userAuth;
}
public User kakaoIdUpdate(Long kakaoId) {
this.kakaoId = kakaoId;
return this;
}
}
회원가입 요청 DTO이다.
/**
* 회원가입 요청 DTO
* 사용자 회원가입을 위한 데이터 전송 객체
*/
@Getter
@NoArgsConstructor
@AllArgsConstructor
public class SignupRequestDto {
@NotBlank(message = "사용자 ID를 입력해주세요")
@Pattern(regexp = "^[a-z0-9]{4,10}$", message = "사용자 ID는 알파벳 소문자와 숫자로 이루어진 4자에서 10자 사이여야 합니다.")
private String userId;
@NotBlank(message = "비밀번호를 입력해주세요")
@Pattern(regexp = "^(?=.*[a-zA-Z])(?=.*\\d)(?=.*[!@#$%^&*()_+\\-=\\[\\]{};':\"\\\\|,.<>\\/?]).{8,15}$", message = "비밀번호는 대소문자 영문, 숫자, 특수문자를 최소 1글자씩 포함하며 최소 8자에서 15자 사이여야 합니다.")
private String password;
@NotBlank(message = "사용자 이름을 입력해주세요")
private String userName;
@NotBlank(message = "이메일을 입력해주세요")
@Email(message = "이메일 형식으로 입력해주세요.")
private String email;
private String intro;
private String adminToken;
public String getAdminToken() {
return adminToken != null ? adminToken : "";
}
}
로그인 요청 DTO이다.
@Getter
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class LoginRequestDto {
@NotBlank(message = "사용자 ID를 입력해주세요")
private String userId;
@NotBlank(message = "비밀번호를 입력해주세요")
private String password;
}
exception은 이렇게 정리했다.
@Slf4j
@RestControllerAdvice
@RestController
public class GlobalExceptionHandler {
/**
* 클라이언트로 부터 잘못된 요청 값이 들어온 경우
* @param ex : BadRequestException[custom]
* @return : error message, HttpStatus.BAD_REQUEST => 400
*/
@ExceptionHandler(BadRequestException.class)
public ResponseEntity<String> badRequestException(BadRequestException ex) {
log.error("{}", ex.getMessage());
return new ResponseEntity<>(ex.getMessage(), HttpStatus.BAD_REQUEST);
}
/**
* DB 조회 시 값이 존재하지 않는 경우
* @param ex : DataNotFoundException[custom]
* @return : error message, HttpStatus.NOT_FOUND => 404
*/
@ExceptionHandler(DataNotFoundException.class)
public ResponseEntity<String> dataNotFoundException(DataNotFoundException ex) {
log.error("{}", ex.getMessage());
return new ResponseEntity<>(ex.getMessage(), HttpStatus.NOT_FOUND);
}
/**
* 이미 존재하는 값이 있는 경우
* @param ex : ConflictException[custom]
* @return : error message, HttpStatus.CONFLICT => 409
*/
@ExceptionHandler(ConflictException.class)
public ResponseEntity<String> conflictException(ConflictException ex) {
log.error("{}", ex.getMessage());
return new ResponseEntity<>(ex.getMessage(), HttpStatus.CONFLICT);
}
/**
* 클라이언트가 유효한 인증 제공을 하지 못한 경우
* @param ex : UnauthorizedException[custom]
* @return : error message, HttpStatus.UNAUTHORIZED => 401
*/
@ExceptionHandler(UnauthorizedException.class)
public ResponseEntity<String> unauthorizedException(UnauthorizedException ex) {
log.error("{}", ex.getMessage());
return new ResponseEntity<>(ex.getMessage(), HttpStatus.UNAUTHORIZED);
}
/**
* 인증은 되었으나 접근 권한이 없는 경우
* @param ex : ForbiddenException[custom]
* @return : error message, HttpStatus.FORBIDDEN => 403
*/
@ExceptionHandler(ForbiddenException.class)
public ResponseEntity<String> forbiddenException(ForbiddenException ex) {
log.error("{}", ex.getMessage());
return new ResponseEntity<>(ex.getMessage(), HttpStatus.FORBIDDEN);
}
/**
* Valid 유효성 검사 에러 ( 잘못된 요청에 대한 값이 들어온 경우 )
* @param ex : MethodArgumentNotValidException
* @return : error message, HttpStatus.BAD_REQUEST => 400
*/
@ExceptionHandler(MethodArgumentNotValidException.class)
public ResponseEntity<Map<String, String>> methodArgumentNotValidException(MethodArgumentNotValidException ex) {
log.error("{}", ex.getMessage());
Map<String, String> errors = new HashMap<>();
for (FieldError fieldError : ex.getBindingResult().getFieldErrors()) {
errors.put(fieldError.getField(), fieldError.getDefaultMessage());
}
return new ResponseEntity<>(errors, HttpStatus.BAD_REQUEST);
}
/**
* 중복된 좋아요&팔로우가 눌렸을 경우[unique 제약 조건 위배 에러]
* @param ex : ViolatedException[custom]
* @return : HttpStatus.CONFLICT => 409 : 클라이언트 요청에 대한 서버간 충돌
*/
@ExceptionHandler(ViolatedException.class)
public ResponseEntity<String> violatedLikeException(ViolatedException ex) {
log.error("{}", ex.getMessage());
return new ResponseEntity<>(ex.getMessage(), HttpStatus.CONFLICT);
}
/**
* 입력된 값이 유효하지 않은 경우
* @param ex : InvalidEnteredException[custom]
* @return : error message, HttpStatus.BAD_REQUEST => 400
*/
@ExceptionHandler(InvalidEnteredException.class)
public ResponseEntity<String> invalidEnteredException(InvalidEnteredException ex) {
log.error("{}", ex.getMessage());
return new ResponseEntity<>(ex.getMessage(), HttpStatus.BAD_REQUEST);
}
}
'TIL' 카테고리의 다른 글
TIL - 2024/06/21 (0) | 2024.06.21 |
---|---|
TIL - 2024/06/20 (0) | 2024.06.20 |
TIL - 2024/06/18 (0) | 2024.06.18 |
TIL - 2024/06/17 (0) | 2024.06.17 |
TIL - 2024/06/14 (0) | 2024.06.14 |