2026-01-19 14:45:08 +09:00

4.3 KiB

SMS 발송/인증 적용 메뉴얼 (Laravel)

이 문서는 SMS 인증번호 발송/검증을 프로젝트에서 일관되게 적용하기 위한 기준을 정리합니다.

목적: 자동 입력/도배 방지 + 인증 품질 향상 + 운영 중 추적 가능(로그/레이트리밋)


0. 권장 설계 개요

  • 발송(send) / 검증(verify) API를 분리
  • 휴대폰 번호는 서버에서 표준화(normalize) 후 저장/비교
  • 과도한 시도 방지: RateLimiter + (선택) IP/기기 세션 제한
  • 코드 저장: (권장) 캐시/DB 모두 가능
    • 캐시(예: Redis) 사용 시 만료(TTL) 관리가 쉬움
    • DB 저장은 감사/추적(감사로그)에 유리

1. 엔드포인트 권장 스펙

1.1 인증번호 발송

  • POST /auth/phone/send-code
  • request
    • phone (string)
    • g-recaptcha-response (string, v3 권장)
  • response
    • { ok: true } 또는 { ok: false, message }

1.2 인증번호 검증

  • POST /auth/phone/verify-code
  • request
    • phone
    • code
    • g-recaptcha-response
  • response
    • { ok: true, redirect: ... } 또는 { ok: false, message }

2. 번호 표준화 (필수)

서버에서 입력값을 한국 휴대폰 숫자만으로 정규화:

  • 입력: 010-1234-5678, 010 1234 5678, +82 10-1234-5678
  • 저장/비교: 01012345678

권장 함수 예시:

private function normalizeKoreanPhone(string $raw): ?string
{
    $digits = preg_replace('/\D+/', '', $raw);
    if (!$digits) return null;

    // +82 처리(예: 8210xxxxxxxx)
    if (str_starts_with($digits, '82')) {
        $digits = '0' . substr($digits, 2);
    }

    // 010/011/... 휴대폰 기준(프로젝트 정책에 맞게 조정)
    if (!preg_match('/^01[016789]\d{7,8}$/', $digits)) {
        return null;
    }

    // 최종 10~11자리
    if (strlen($digits) < 10 || strlen($digits) > 11) return null;

    return $digits;
}

3. RateLimiter 정책 (권장)

3.1 휴대폰 기준 발송 제한

예: 10분에 5회

  • key: sms:send:{phone}
  • limit: 5
  • decay: 600초

3.2 IP 기준 제한(선택)

  • key: sms:sendip:{ip}

3.3 코드 검증 시도 제한

예: 10분에 10회

  • key: sms:verify:{phone}

4. 인증코드 생성/저장/만료

4.1 코드 생성

  • 6자리 숫자
  • 예: random_int(100000, 999999)

4.2 저장(권장: Cache/Redis)

  • key: sms:code:{phone}
  • value: 해시 저장 권장
  • ttl: 180초~300초

예시:

$code = (string) random_int(100000, 999999);
$hash = hash_hmac('sha256', $code, config('app.key'));

Cache::put("sms:code:{$phone}", [
  'hash' => $hash,
  'created_at' => now()->toDateTimeString(),
], now()->addMinutes(3));

검증 시:

$stored = Cache::get("sms:code:{$phone}");
if (!$stored) { /* 만료 */ }

$hash = hash_hmac('sha256', $inputCode, config('app.key'));
if (!hash_equals($stored['hash'], $hash)) { /* 실패 */ }

Cache::forget("sms:code:{$phone}"); // 성공 시 1회용

5. SMS Provider 연동 위치

  • 실제 발송은 app/Services/SmsService.php 같은 서비스 레이어로 분리 권장
  • 컨트롤러는:
    1. validate
    2. rate limit
    3. code 생성/저장
    4. SmsService 호출
    5. 응답 반환

6. 로깅/감사(권장)

  • 민감정보는 최소화
    • 전화번호는 마스킹 로그 권장: 010****5678
    • 인증코드 원문 로그 금지

권장 전용 로그 파일:

  • storage/logs/sms.log

(logging channel을 별도 구성하면 recaptcha와 동일한 방식으로 분리 가능)


7. 보안 체크리스트

  • reCAPTCHA(v3) 적용 (send/verify 모두)
  • RateLimiter 적용
  • 인증코드 TTL 적용
  • 성공 시 1회용 처리(Cache forget)
  • 실패 횟수 제한(verify)
  • 동일 번호 반복 요청 시 UX 메시지(남은 시간 안내 등)
  • 운영 로그 분리(필요 시)

8. API 응답 메시지 정책(권장)

공격자에게 힌트가 되지 않게, 실패 메시지는 통일:

  • 발송 실패: 처리에 실패했습니다. 잠시 후 다시 시도해 주세요.
  • 검증 실패: 인증번호가 올바르지 않습니다. (횟수 초과/만료는 별도 가능)

9. 운영 튜닝 포인트

  • 발송 제한(10분 5회)은 초기 보수적으로 시작 → 운영 로그 보고 조정
  • 통신사/지연으로 인증 도착이 늦어질 수 있으니 TTL 3~5분 권장