이메일 인증 코드 발송 로직 및 구현

인증 코드 발송 처리 흐름

시스템은 사용자에게 이메일 인증 코드를 전송하기 전, 여러 단계의 제한 조건을 검사하여 과도한 요청을 방지합니다.

1. 첫 번째 수준의 요청 제한 확인

해당 전화번호에 대해 최근 5분 내에 이미 요청이 있었는지를 확인합니다. Redis의 Set 자료구조를 이용하며, 키는 "limit:first_level:{전화번호}", 값은 "1"로 저장됩니다.

Boolean firstLevelBlocked = redisTemplate.opsForSet().isMember(FIRST_LEVEL_LIMIT_PREFIX + phoneNumber, "1");
if (firstLevelBlocked != null && firstLevelBlocked) {
    return Result.fail("5분 후 다시 시도해주세요.");
}

2. 두 번째 수준의 요청 제한 확인

최근 20분 동안 특정 횟수 이상 요청한 경우 적용되는 제한입니다. 마찬가지로 Redis Set을 활용하며 키는 "limit:second_level:{전화번호}"입니다.

Boolean secondLevelBlocked = redisTemplate.opsForSet().isMember(SECOND_LEVEL_LIMIT_PREFIX + phoneNumber, "1");
if (secondLevelBlocked != null && secondLevelBlocked) {
    return Result.fail("20분 후 다시 시도해주세요.");
}

3. 1분당 한 번만 발송 가능

Redis ZSet을 사용해 해당 번호로 지난 1분간 발송된 기록을 카운트합니다. 하나라도 존재하면 재발송을 막습니다.

long oneMinuteAgo = System.currentTimeMillis() - 60_000L;
long recentRequests = redisTemplate.opsForZSet().count(SEND_TIME_KEY + phoneNumber, oneMinuteAgo, System.currentTimeMillis());
if (recentRequests >= 1) {
    return Result.fail("1분 이내에는 중복 발송이 불가능합니다.");
}

4. 5분 내 발송 횟수와 제한 설정

5분간 누적 발송 횟수가 특정 조건(예: 5회 도달 또는 3n+2 패턴)을 만족할 경우, 상위 제한 규칙을 활성화합니다.

long fiveMinutesAgo = System.currentTimeMillis() - 300_000L;
long requestCount = redisTemplate.opsForZSet().count(SEND_TIME_KEY + phoneNumber, fiveMinutesAgo, System.currentTimeMillis());

if (requestCount % 3 == 2 && requestCount > 5) {
    // 8, 11, 14... 회차마다 20분간 제한
    redisTemplate.opsForSet().add(SECOND_LEVEL_LIMIT_PREFIX + phoneNumber, "1");
    redisTemplate.expire(SECOND_LEVEL_LIMIT_PREFIX + phoneNumber, 20, TimeUnit.MINUTES);
    return Result.fail("과도한 요청으로 20분간 제한됩니다.");
} else if (requestCount == 5) {
    // 다섯 번째 요청 시 5분간 제한
    redisTemplate.opsForSet().add(FIRST_LEVEL_LIMIT_PREFIX + phoneNumber, "1");
    redisTemplate.expire(FIRST_LEVEL_LIMIT_PREFIX + phoneNumber, 5, TimeUnit.MINUTES);
    return Result.fail("5분간 더 이상 요청할 수 없습니다.");
}

5. 인증 코드 생성 및 Redis 저장

유효한 요청이라면 난수 인증 코드를 생성하고, 이를 Redis에 2분간 보관합니다.

String verificationCode = VerificationUtil.generateCode();
redisTemplate.opsForValue().set(CODE_STORAGE_PREFIX + phoneNumber, verificationCode, CODE_EXPIRY_MINUTES, TimeUnit.MINUTES);

6. 이메일 발송 및 시간 기록

생성된 코드를 실제 메일로 발송하고, 현재 시간을 기준으로 ZSet에 발송 시간을 기록합니다.

EmailService.sendVerificationCode(emailAddress, verificationCode);

String timestamp = String.valueOf(System.currentTimeMillis());
redisTemplate.opsForZSet().add(SEND_TIME_KEY + phoneNumber, timestamp, System.currentTimeMillis());

전체 서비스 메서드 예시

@Override
public Result triggerEmailVerification(String email, String phone) throws MessagingException {
    // 1단계 제한 체크
    if (Boolean.TRUE.equals(redisTemplate.opsForSet().isMember(FIRST_LEVEL_LIMIT_PREFIX + phone, "1"))) {
        return Result.fail("5분 후 다시 시도해주세요.");
    }

    // 2단계 제한 체크
    if (Boolean.TRUE.equals(redisTemplate.opsForSet().isMember(SECOND_LEVEL_LIMIT_PREFIX + phone, "1"))) {
        return Result.fail("20분 후 다시 시도해주세요.");
    }

    // 1분당 발송 횟수 체크
    long now = System.currentTimeMillis();
    long oneMinBack = now - 60_000L;
    if (redisTemplate.opsForZSet().count(SEND_TIME_KEY + phone, oneMinBack, now) >= 1) {
        return Result.fail("1분 이내에는 중복 발송이 불가능합니다.");
    }

    // 5분간 누적 요청 수 확인 및 제한 업데이트
    long fiveMinBack = now - 300_000L;
    long totalRequests = redisTemplate.opsForZSet().count(SEND_TIME_KEY + phone, fiveMinBack, now);

    if (totalRequests % 3 == 2 && totalRequests > 5) {
        redisTemplate.opsForSet().add(SECOND_LEVEL_LIMIT_PREFIX + phone, "1");
        redisTemplate.expire(SECOND_LEVEL_LIMIT_PREFIX + phone, 20, TimeUnit.MINUTES);
        return Result.fail("과도한 요청으로 20분간 제한됩니다.");
    } else if (totalRequests == 5) {
        redisTemplate.opsForSet().add(FIRST_LEVEL_LIMIT_PREFIX + phone, "1");
        redisTemplate.expire(FIRST_LEVEL_LIMIT_PREFIX + phone, 5, TimeUnit.MINUTES);
        return Result.fail("5분간 더 이상 요청할 수 없습니다.");
    }

    // 인증 코드 생성 및 저장
    String code = VerificationUtil.generateCode();
    redisTemplate.opsForValue().set(CODE_STORAGE_PREFIX + phone, code, CODE_EXPIRY_MINUTES, TimeUnit.MINUTES);

    // 이메일 발송
    EmailService.sendVerificationCode(email, code);

    // 발송 시간 기록
    redisTemplate.opsForZSet().add(SEND_TIME_KEY + phone, String.valueOf(now), now);

    return Result.ok();
}

태그: Redis Spring Boot java authentication Rate Limiting

8월 7일 06:50에 게시됨