first commit
This commit is contained in:
36
backend/core/rate_limit.py
Normal file
36
backend/core/rate_limit.py
Normal file
@@ -0,0 +1,36 @@
|
||||
"""Rate limit на основе Redis `INCR`+`EXPIRE` для публичных (без auth) эндпоинтов.
|
||||
|
||||
Используется резолвом конференций и гостевым входом (`api/conferences.py`) —
|
||||
эндпоинтами без аутентификации, уязвимыми к перебору номера/ссылки конференции
|
||||
(см. ADR-001, п.4 — оценка энтропии и рекомендуемый лимит 10 запросов/мин на IP).
|
||||
"""
|
||||
|
||||
from fastapi import HTTPException, status
|
||||
|
||||
from core.redis import redis_client
|
||||
|
||||
RATE_LIMIT_MAX_REQUESTS = 10
|
||||
RATE_LIMIT_WINDOW_SECONDS = 60
|
||||
|
||||
|
||||
async def enforce_rate_limit(
|
||||
key: str,
|
||||
*,
|
||||
max_requests: int = RATE_LIMIT_MAX_REQUESTS,
|
||||
window_seconds: int = RATE_LIMIT_WINDOW_SECONDS,
|
||||
) -> None:
|
||||
"""Увеличить счётчик запросов по ключу; бросить 429, если лимит превышен.
|
||||
|
||||
`INCR` атомарно создаёт ключ со значением 1, если его ещё не было; TTL
|
||||
выставляется только при первом инкременте в окне (когда счётчик стал
|
||||
равен 1) — иначе окно продлевалось бы при каждом запросе и лимит
|
||||
никогда бы не истекал.
|
||||
"""
|
||||
redis_key = f"rate_limit:{key}"
|
||||
current = await redis_client.incr(redis_key)
|
||||
if current == 1:
|
||||
await redis_client.expire(redis_key, window_seconds)
|
||||
if current > max_requests:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_429_TOO_MANY_REQUESTS, detail="rate_limit_exceeded"
|
||||
)
|
||||
Reference in New Issue
Block a user