""" GUARDiA ITSM — API Rate Limiting (Enhancement F-3) 기능: 1. SlowAPI 기반 FastAPI 미들웨어 통합 2. IP 기반 기본 제한 + JWT 사용자 식별 제한 3. 엔드포인트별 차등 제한 설정 4. Redis 백엔드 (미설치 시 메모리 폴백) 5. X-RateLimit-* 응답 헤더 자동 추가 6. 429 Too Many Requests 시 재시도 안내 제한 설정: - 기본: 60 req/min per IP - 로그인: 10 req/min per IP (brute-force 방지) - 일반 API: 120 req/min per user - AI/LLM 엔드포인트: 10 req/min per user (비용 보호) - 파일 업로드: 20 req/min per user 사용 예:: from core.ratelimit import limiter, DEFAULT_LIMIT @router.get("/items") @limiter.limit(DEFAULT_LIMIT) async def list_items(request: Request, ...): ... """ from __future__ import annotations import logging import os from typing import Callable, Optional logger = logging.getLogger(__name__) # ── 제한 상수 ───────────────────────────────────────────────────────────────── DEFAULT_LIMIT = "120/minute" # 일반 API (인증된 사용자) STRICT_LIMIT = "60/minute" # 기본 IP 제한 LOGIN_LIMIT = "10/minute" # 로그인 엔드포인트 AI_LIMIT = "10/minute" # LLM/AI 엔드포인트 UPLOAD_LIMIT = "20/minute" # 파일 업로드 ADMIN_LIMIT = "300/minute" # ADMIN 사용자 (완화) # ── SlowAPI 초기화 ──────────────────────────────────────────────────────────── _limiter: Optional[object] = None def _get_real_ip(request: object) -> str: """ 클라이언트 실제 IP 추출. 리버스 프록시(X-Forwarded-For) 우선, 없으면 직접 IP. """ forwarded = getattr(getattr(request, "headers", None), "get", lambda k: None)("X-Forwarded-For") if forwarded: return forwarded.split(",")[0].strip() client = getattr(request, "client", None) if client: return getattr(client, "host", "unknown") return "unknown" def _get_user_key(request: object) -> str: """ Rate limit 키 결정. Authorization 헤더에서 JWT sub(사용자명)을 추출하여 사용. 없으면 IP 주소로 폴백. """ auth = getattr(getattr(request, "headers", None), "get", lambda k: None)("Authorization") if auth and auth.startswith("Bearer "): try: from jose import jwt from core.auth import SECRET_KEY, ALGORITHM payload = jwt.decode(auth[7:], SECRET_KEY, algorithms=[ALGORITHM]) sub = payload.get("sub") if sub: return f"user:{sub}" except Exception: pass return f"ip:{_get_real_ip(request)}" def create_limiter(): """SlowAPI 리미터 생성. slowapi 미설치 시 더미 리미터 반환.""" global _limiter if _limiter is not None: return _limiter try: from slowapi import Limiter from slowapi.util import get_remote_address # Redis 사용 시 slowapi storage URL 설정 redis_url = os.environ.get("REDIS_URL", "") storage_uri = redis_url if redis_url and "redis" in redis_url else "memory://" _limiter = Limiter( key_func=_get_user_key, default_limits=[STRICT_LIMIT], storage_uri=storage_uri, strategy="fixed-window", ) logger.info("Rate limiter 초기화: storage=%s", storage_uri) return _limiter except ImportError: logger.info("slowapi 미설치 — Rate limiting 비활성화 (더미 리미터 사용)") _limiter = _DummyLimiter() return _limiter class _DummyLimiter: """slowapi 미설치 시 사용하는 no-op 리미터.""" def limit(self, limit_str: str, *args, **kwargs): """데코레이터 호출 시 그냥 원본 함수 반환.""" def decorator(func: Callable) -> Callable: return func return decorator def shared_limit(self, *args, **kwargs): def decorator(func: Callable) -> Callable: return func return decorator # ── 싱글톤 접근 ─────────────────────────────────────────────────────────────── limiter = create_limiter() # ── FastAPI 앱 통합 헬퍼 ────────────────────────────────────────────────────── def setup_rate_limiting(app) -> None: """ FastAPI 앱에 SlowAPI 미들웨어 및 예외 핸들러 등록. main.py 의 앱 생성 직후 호출:: from core.ratelimit import setup_rate_limiting setup_rate_limiting(app) """ try: from slowapi import _rate_limit_exceeded_handler from slowapi.errors import RateLimitExceeded from slowapi.middleware import SlowAPIMiddleware app.state.limiter = limiter app.add_exception_handler(RateLimitExceeded, _rate_limit_exceeded_handler) app.add_middleware(SlowAPIMiddleware) logger.info("SlowAPI Rate Limiting 미들웨어 등록 완료") except ImportError: logger.info("slowapi 미설치 — Rate Limiting 미들웨어 스킵") except Exception as exc: logger.warning("Rate Limiting 설정 실패 (무시): %s", exc) # ── 관리자 API: 현재 제한 상태 조회 ────────────────────────────────────────── async def get_rate_limit_status(user_key: str) -> dict: """특정 사용자/IP의 현재 Rate Limit 상태 조회 (관리자 전용).""" try: from slowapi import Limiter if isinstance(limiter, _DummyLimiter): return {"status": "disabled", "message": "slowapi 미설치"} return { "status": "enabled", "key": user_key, "limits": { "default": DEFAULT_LIMIT, "login": LOGIN_LIMIT, "ai": AI_LIMIT, "upload": UPLOAD_LIMIT, }, } except Exception: return {"status": "unknown"}