diff --git a/LifeMaster-BE/src/main/java/com/example/LifeMaster_BE/UserManager/Email/Password/Dto/VerifyCodeRequest.java b/LifeMaster-BE/src/main/java/com/example/LifeMaster_BE/UserManager/Email/Password/Dto/VerifyCodeRequest.java new file mode 100644 index 0000000..86fac0a --- /dev/null +++ b/LifeMaster-BE/src/main/java/com/example/LifeMaster_BE/UserManager/Email/Password/Dto/VerifyCodeRequest.java @@ -0,0 +1,11 @@ +package com.example.LifeMaster_BE.UserManager.Email.Password.Dto; + +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Getter +@NoArgsConstructor +public class VerifyCodeRequest { + private String email; + private String code; +} diff --git a/LifeMaster-BE/src/main/java/com/example/LifeMaster_BE/UserManager/Email/Password/PasswordController.java b/LifeMaster-BE/src/main/java/com/example/LifeMaster_BE/UserManager/Email/Password/PasswordController.java index eb78204..f4cc659 100644 --- a/LifeMaster-BE/src/main/java/com/example/LifeMaster_BE/UserManager/Email/Password/PasswordController.java +++ b/LifeMaster-BE/src/main/java/com/example/LifeMaster_BE/UserManager/Email/Password/PasswordController.java @@ -4,6 +4,7 @@ import com.example.LifeMaster_BE.UserManager.Email.Password.Dto.PasswordResetDto; import com.example.LifeMaster_BE.UserManager.Email.Password.Dto.PasswordResponseDto; import com.example.LifeMaster_BE.UserManager.Email.Password.Dto.TokenVerifyResponse; +import com.example.LifeMaster_BE.UserManager.Email.Password.Dto.VerifyCodeRequest; import lombok.RequiredArgsConstructor; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.*; @@ -16,24 +17,24 @@ public class PasswordController { private final PasswordService passwordService; @PostMapping("/confirm-email") - public ResponseEntity sendEmail(@RequestBody ConfirmEmailRequest request){ + public ResponseEntity sendEmail(@RequestBody ConfirmEmailRequest request) { passwordService.sendPasswordResetEmail(request.getEmail()); - return ResponseEntity.ok(new PasswordResponseDto(true, "Password reset link has been sent to your email.")); + return ResponseEntity.ok(new PasswordResponseDto(true, "인증 코드가 이메일로 발송되었습니다.")); } - @GetMapping("/verify") - public ResponseEntity verifyToken(@RequestParam String token) { - passwordService.verifyToken(token); - return ResponseEntity.ok(new TokenVerifyResponse(true, "Token verified.", token)); + @PostMapping("/verify-code") + public ResponseEntity verifyCode(@RequestBody VerifyCodeRequest request) { + String token = passwordService.verifyCode(request.getEmail(), request.getCode()); + return ResponseEntity.ok(new TokenVerifyResponse(true, "인증이 완료되었습니다.", token)); } @PostMapping - public ResponseEntity resetPassword(@RequestBody PasswordResetDto passwordResetDto){ + public ResponseEntity resetPassword(@RequestBody PasswordResetDto passwordResetDto) { String token = passwordResetDto.getToken(); String newPassword = passwordResetDto.getNewPassword(); String checkPassword = passwordResetDto.getCheckPassword(); passwordService.resetPassword(token, newPassword, checkPassword); - return ResponseEntity.ok(new PasswordResponseDto(true, "password reset successful.")); + return ResponseEntity.ok(new PasswordResponseDto(true, "비밀번호가 성공적으로 재설정되었습니다.")); } } diff --git a/LifeMaster-BE/src/main/java/com/example/LifeMaster_BE/UserManager/Email/Password/PasswordService.java b/LifeMaster-BE/src/main/java/com/example/LifeMaster_BE/UserManager/Email/Password/PasswordService.java index 9f77320..935f2d7 100644 --- a/LifeMaster-BE/src/main/java/com/example/LifeMaster_BE/UserManager/Email/Password/PasswordService.java +++ b/LifeMaster-BE/src/main/java/com/example/LifeMaster_BE/UserManager/Email/Password/PasswordService.java @@ -23,33 +23,32 @@ public void sendPasswordResetEmail(String email) { MemberEntity memberEntity = memberRepository.findByEmail(email) .orElseThrow(() -> new EntityNotFoundException("존재하지 않는 이메일 정보입니다.")); - String token = tokenService.createToken(memberEntity.getId()); - String resetLink = "https://api.lifemaster.harvester.kr/auth/password/reset/verify?token=" + token; -// String resetLink = "http://localhost:8080/auth/password/reset/verify?token=" + token; - // HTML 형식으로 작성 + String code = tokenService.createVerificationCode(email, memberEntity.getId()); + String htmlContent = String.format( "" + "" + "

비밀번호 재설정

" + - "

아래 링크를 클릭하여 비밀번호를 재설정하세요:

" + - "비밀번호 재설정" + - "

링크가 작동하지 않으면 다음 URL을 복사하여 브라우저에 붙여넣으세요:

" + - "

%s

" + + "

아래 인증 코드를 입력하여 비밀번호를 재설정하세요:

" + + "
" + + "%s" + + "
" + + "

이 코드는 10분 동안 유효합니다.

" + + "

본인이 요청하지 않았다면 이 이메일을 무시해주세요.

" + "" + "", - resetLink, resetLink + code ); - emailService.sendEmail(email, "비밀번호 재설정", htmlContent); + emailService.sendEmail(email, "비밀번호 재설정 인증 코드", htmlContent); } - public void verifyToken(String token){ - tokenService.validateToken(token); + public String verifyCode(String email, String code) { + return tokenService.verifyCodeAndCreateToken(email, code); } - public void resetPassword(String token, String newPassword, String checkPassword){ - - if(!confirmPassword(newPassword, checkPassword)){ + public void resetPassword(String token, String newPassword, String checkPassword) { + if (!confirmPassword(newPassword, checkPassword)) { throw new IllegalArgumentException("비밀번호가 일치하지 않습니다"); } @@ -64,8 +63,7 @@ public void resetPassword(String token, String newPassword, String checkPassword memberRepository.save(memberEntity); } - private boolean confirmPassword(String password, String confirmPassword){ + private boolean confirmPassword(String password, String confirmPassword) { return password.equals(confirmPassword); } - } diff --git a/LifeMaster-BE/src/main/java/com/example/LifeMaster_BE/UserManager/Email/Password/TokenService.java b/LifeMaster-BE/src/main/java/com/example/LifeMaster_BE/UserManager/Email/Password/TokenService.java index 73f9b07..8f5dc8d 100644 --- a/LifeMaster-BE/src/main/java/com/example/LifeMaster_BE/UserManager/Email/Password/TokenService.java +++ b/LifeMaster-BE/src/main/java/com/example/LifeMaster_BE/UserManager/Email/Password/TokenService.java @@ -5,6 +5,7 @@ import org.springframework.stereotype.Service; import java.time.Duration; +import java.util.Random; import java.util.UUID; @Service @@ -12,25 +13,94 @@ public class TokenService { private final StringRedisTemplate redisTemplate; + private static final Duration CODE_EXPIRATION = Duration.ofMinutes(10); private static final Duration TOKEN_EXPIRATION = Duration.ofMinutes(10); + private static final int MAX_ATTEMPTS = 5; + private static final String CODE_KEY_PREFIX = "password_reset:code:"; + private static final String ATTEMPTS_KEY_PREFIX = "password_reset:attempts:"; + private static final String USERID_KEY_PREFIX = "password_reset:userid:"; - public String createToken(Long userId){ - String token = UUID.randomUUID().toString(); - redisTemplate.opsForValue().set(token, String.valueOf(userId), TOKEN_EXPIRATION); - return token; + // 6자리 인증 코드 생성 및 저장 + public String createVerificationCode(String email, Long userId) { + String code = generateSixDigitCode(); + String codeKey = CODE_KEY_PREFIX + email; + String attemptsKey = ATTEMPTS_KEY_PREFIX + email; + String userIdKey = USERID_KEY_PREFIX + email; + + redisTemplate.opsForValue().set(codeKey, code, CODE_EXPIRATION); + redisTemplate.opsForValue().set(attemptsKey, "0", CODE_EXPIRATION); + redisTemplate.opsForValue().set(userIdKey, String.valueOf(userId), CODE_EXPIRATION); + + return code; + } + + // 6자리 숫자 코드 생성 + private String generateSixDigitCode() { + Random random = new Random(); + int code = 100000 + random.nextInt(900000); + return String.valueOf(code); } - public void validateToken(String token){ - Boolean hasKey = redisTemplate.hasKey(token); - if (hasKey == null || !hasKey){ - throw new IllegalArgumentException("Invalid token"); + // 인증 코드 검증 및 토큰 발급 + public String verifyCodeAndCreateToken(String email, String inputCode) { + String codeKey = CODE_KEY_PREFIX + email; + String attemptsKey = ATTEMPTS_KEY_PREFIX + email; + String userIdKey = USERID_KEY_PREFIX + email; + + // 저장된 코드 조회 + String storedCode = redisTemplate.opsForValue().get(codeKey); + if (storedCode == null) { + throw new IllegalArgumentException("인증 코드가 만료되었거나 존재하지 않습니다."); + } + + // 시도 횟수 확인 + String attemptsStr = redisTemplate.opsForValue().get(attemptsKey); + int attempts = attemptsStr != null ? Integer.parseInt(attemptsStr) : 0; + + if (attempts >= MAX_ATTEMPTS) { + deleteVerificationData(email); + throw new IllegalArgumentException("인증 시도 횟수를 초과했습니다. 다시 인증 코드를 요청해주세요."); + } + + // 코드 검증 + if (!storedCode.equals(inputCode)) { + redisTemplate.opsForValue().set(attemptsKey, String.valueOf(attempts + 1), CODE_EXPIRATION); + int remainingAttempts = MAX_ATTEMPTS - attempts - 1; + throw new IllegalArgumentException("인증 코드가 일치하지 않습니다. 남은 시도 횟수: " + remainingAttempts); + } + + // 검증 성공 - userId 조회 후 토큰 발급 + String userIdStr = redisTemplate.opsForValue().get(userIdKey); + if (userIdStr == null) { + throw new IllegalArgumentException("사용자 정보를 찾을 수 없습니다."); } + + // 인증 데이터 삭제 + deleteVerificationData(email); + + // 토큰 발급 + return createToken(Long.valueOf(userIdStr)); + } + + // 인증 데이터 삭제 + private void deleteVerificationData(String email) { + redisTemplate.delete(CODE_KEY_PREFIX + email); + redisTemplate.delete(ATTEMPTS_KEY_PREFIX + email); + redisTemplate.delete(USERID_KEY_PREFIX + email); + } + + // 토큰 생성 (비밀번호 재설정용) + public String createToken(Long userId) { + String token = UUID.randomUUID().toString(); + redisTemplate.opsForValue().set(token, String.valueOf(userId), TOKEN_EXPIRATION); + return token; } - public Long validateAndConsumeToken(String token){ + // 토큰 검증 및 소비 + public Long validateAndConsumeToken(String token) { String userIdStr = redisTemplate.opsForValue().get(token); - if(userIdStr == null){ - throw new IllegalArgumentException("Invalid token"); + if (userIdStr == null) { + throw new IllegalArgumentException("유효하지 않거나 만료된 토큰입니다."); } redisTemplate.delete(token); return Long.valueOf(userIdStr);