"""API 보안 모듈 - API 키 기반 인증 (Bearer token) - 매매 실행 및 관리 API 보호 - 읽기 전용 엔드포인트는 인증 선택적 사용법: from app.core.security import require_api_key, optional_api_key @router.post("/execute", dependencies=[Depends(require_api_key)]) async def execute_trade(...): ... @router.get("/data") async def get_data(api_key: str | None = Depends(optional_api_key)): ... """ import hmac import logging import secrets from fastapi import Depends, HTTPException, Security, status from fastapi.security import APIKeyHeader from app.core.config import settings logger = logging.getLogger(__name__) # API 키 헤더 스키마 _api_key_header = APIKeyHeader(name="X-API-Key", auto_error=False) def _constant_time_compare(a: str, b: str) -> bool: """타이밍 공격 방지를 위한 상수 시간 비교.""" return hmac.compare_digest(a.encode(), b.encode()) def _get_api_key() -> str: """API 인증용 키 반환. API_KEY가 설정되면 우선 사용, 없으면 SECRET_KEY 폴백.""" return settings.API_KEY if settings.API_KEY else settings.SECRET_KEY async def require_api_key(api_key: str | None = Security(_api_key_header)) -> str: """매매 실행, 관리 API 등 민감한 엔드포인트에 필수 적용. X-API-Key 헤더가 없거나 불일치하면 401 반환. """ if not api_key: logger.warning("[Security] API 키 누락 — 요청 거부") raise HTTPException( status_code=status.HTTP_401_UNAUTHORIZED, detail="API 키가 필요합니다. X-API-Key 헤더를 설정하세요.", ) expected = _get_api_key() if not _constant_time_compare(api_key, expected): logger.warning("[Security] API 키 불일치 — 요청 거부") raise HTTPException( status_code=status.HTTP_403_FORBIDDEN, detail="유효하지 않은 API 키입니다.", ) return api_key async def optional_api_key(api_key: str | None = Security(_api_key_header)) -> str | None: """읽기 전용 엔드포인트에서 선택적 인증. 키가 있으면 검증, 없으면 None 반환 (인증 없이 접근 허용). """ if not api_key: return None expected = _get_api_key() if not _constant_time_compare(api_key, expected): raise HTTPException( status_code=status.HTTP_403_FORBIDDEN, detail="유효하지 않은 API 키입니다.", ) return api_key def generate_secure_key(length: int = 64) -> str: """보안 키 생성 유틸리티 (운영 환경 시크릿 생성용).""" return secrets.token_urlsafe(length)