728x90
320x100
🎯 오늘의 목표
- 이메일 인증 실패 시도 횟수 제한 기능
⚙️ 진행한 작업
- 이메일 인증 실패 시도 횟수 제한 기능
🛠️ 개발내용
- 보안과 남용 방지를 위해 필요한 기능으로, 일반적으로는 Redis 같은 인메모리 저장소를 활용해서 이메일 주소별로 제한 시간 동안 실패 횟수를 카운트하고, 기준 초과 시 인증 요청을 차단하는 방식으로 구현한다고 하여 해보기로 함.
구성 요소 | 설명 |
Redis Key | email:fail:<email> 형태로 저장 |
TTL(Time to Live) | 일정 시간 후 자동 삭제 (예: 10분) |
허용 실패 횟수 | 예: 5회 |
차단 메시지 | "인증 요청이 너무 많습니다. 잠시 후 다시 시도해주세요." |
📌 예외 클래스 추가
EmailVerificationLimitExceededException.java
@Getter
public class EmailVerificationLimitExceededException extends RuntimeException {
private final ErrorCode errorCode;
public EmailVerificationLimitExceededException() {
super(ErrorCode.EMAIL_VERIFICATION_TOO_MANY_ATTEMPTS.getMessage());
this.errorCode = ErrorCode.EMAIL_VERIFICATION_TOO_MANY_ATTEMPTS;
}
}
nvalidTokenException.java
@Getter
public class InvalidTokenException extends RuntimeException {
private final ErrorCode errorCode;
public InvalidTokenException() {
super(ErrorCode.INVALID_EMAIL_VERIFICATION_TOKEN.getMessage());
this.errorCode = ErrorCode.INVALID_EMAIL_VERIFICATION_TOKEN;
}
}
📌 ErrorCode에 항목 추가 (ErrorCode.java)
EMAIL_VERIFICATION_TOO_MANY_ATTEMPTS("TOO_MANY_ATTEMPTS", "이메일 인증 시도 횟수를 초과했습니다. 잠시 후 다시 시도해주세요."),
INVALID_EMAIL_VERIFICATION_TOKEN("INVALID_EMAIL_VERIFICATION_TOKEN", "인증 토큰이 유효하지 않습니다."),
📌 EmailVerificationService.java 수정
@Service
@RequiredArgsConstructor
public class EmailVerificationService {
private final RedisTemplate<String, String> redisTemplate;
private final JavaMailSender mailSender;
private final EmailVerificationRepository emailVerificationRepository;
private static final int MAX_FAIL_COUNT = 5;
private static final long FAIL_EXPIRE_MINUTES = 10;
public void sendVerificationEmail(String email) {
String token = UUID.randomUUID().toString();
String redisKey = "email:verify:" + token;
redisTemplate.opsForValue().set(redisKey, email, 10, TimeUnit.MINUTES);
String link = "http://localhost:8080/api/email/verify?token=" + token;
SimpleMailMessage message = new SimpleMailMessage();
message.setTo(email);
message.setSubject("[북북클럽] 이메일 인증");
message.setText("이메일 인증을 위해 아래 링크를 클릭해주세요:\n" + link);
mailSender.send(message);
}
public boolean verifyEmail(String token) {
String redisKey = "email:verify:" + token;
String email = redisTemplate.opsForValue().get(redisKey);
if (email == null) {
throw new InvalidTokenException(); // 유효하지 않은 토큰 예외
}
checkFailLimit(email); // 실패 횟수 체크
redisTemplate.delete(redisKey); // 사용한 토큰 제거
Optional<EmailVerification> optional = emailVerificationRepository.findByEmail(email);
EmailVerification verification = optional
.map(existing -> {
existing.markAsVerified();
return existing;
})
.orElseGet(() -> EmailVerification.builder()
.email(email)
.verified(true)
.verifiedAt(LocalDateTime.now())
.build());
verification.markAsVerified();
emailVerificationRepository.save(verification);
resetFailCount(email); // 성공 시 실패 카운트 초기화
return true;
}
public boolean isEmailVerified(String email) {
return emailVerificationRepository.existsByEmailAndVerifiedIsTrue(email);
}
private void checkFailLimit(String email) {
String failKey = "email:fail:" + email;
Long count = redisTemplate.opsForValue().increment(failKey);
if (count == 1) {
redisTemplate.expire(failKey, FAIL_EXPIRE_MINUTES, TimeUnit.MINUTES);
}
if (count >= MAX_FAIL_COUNT) {
throw new EmailVerificationLimitExceededException();
}
}
private void resetFailCount(String email) {
String failKey = "email:fail:" + email;
redisTemplate.delete(failKey);
}
}
📌 GlobalExceptionHandler.java 수정
package ddururi.bookbookclub.global.exception;
import ddururi.bookbookclub.global.common.ApiResponse;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
@RestControllerAdvice
public class GlobalExceptionHandler {
//생략//
// ✅ 추가: 유효하지 않은 이메일 인증 토큰
@ExceptionHandler(InvalidTokenException.class)
public ResponseEntity<ApiResponse<Void>> handleInvalidToken(InvalidTokenException e) {
return ResponseEntity.status(HttpStatus.BAD_REQUEST)
.body(ApiResponse.fail(e.getErrorCode()));
}
// ✅ 추가: 이메일 인증 시도 횟수 초과
@ExceptionHandler(EmailVerificationLimitExceededException.class)
public ResponseEntity<ApiResponse<Void>> handleEmailVerificationLimit(EmailVerificationLimitExceededException e) {
return ResponseEntity.status(HttpStatus.TOO_MANY_REQUESTS) // 429
.body(ApiResponse.fail(e.getErrorCode()));
}
}
728x90
320x100
'💻 뚝딱뚝딱 > 북북클럽' 카테고리의 다른 글
[개발일지 #017] 북(Book) 도메인 API 구현 및 테스트 (0) | 2025.04.29 |
---|---|
[개발일지 #016] 북(Book) 도메인 개발 및 단위 테스트 (0) | 2025.04.29 |
[개발일지 #014] 글로벌 예외처리 및 API 응답 포맷 통일 (0) | 2025.04.24 |
[개발일지 #013] 회원 프로필 사진 등록 기능 구현 (0) | 2025.04.24 |
[개발일지 #012] Oauth 로그인 구현 (네이버) (0) | 2025.04.24 |