Перейти к содержимому

Уведомления об аномальном трафике и абьюзе API

Пока проект маленький, полноценной защиты от абьюза обычно нет. А между тем один клиент, разгоняющий ваш эндпойнт, — это и счёт за трафик, и упавший сервис, и потенциальный скрейпинг. Ставим лёгкую middleware: считаем запросы по IP или API-ключу и шлём пуш, когда кто-то резко разгоняется или пошла волна 429.

  • Только на сервере. Считаем на бэкенде, notify() вызываем оттуда.
  • Тротлинг самих пушей. Во время атаки не превращайте Notifly в тот же шторм — один пуш на клиента раз в N минут.
  • Отличайте всплеск от запуска. Резкий рост трафика может быть не абьюзом, а вашим же маркетинговым запуском. Смотрите на концентрацию: один IP/ключ даёт большую долю запросов — это абьюз; трафик равномерно размазан по многим адресам — это, скорее, наплыв реальных людей.

Берём notify() из обзорной страницы:

import os, requests
def notify(title, message, priority=5):
requests.post(
f"{os.environ['NOTIFLY_URL']}/message",
params={"token": os.environ["NOTIFLY_TOKEN"]},
json={"title": title, "message": message, "priority": priority},
timeout=5,
)

Скользящее окно на IP; пуш, когда один клиент пробивает лимит:

const {notify} = require('./notifly');
const WINDOW_MS = 60_000; // окно 1 минута
const LIMIT = 300; // порог запросов от одного клиента за окно
const NOTIFY_COOLDOWN = 300_000;
const hits = new Map(); // ip -> [timestamps]
const lastNotify = new Map(); // ip -> ts
function rateGuard(req, res, next) {
const ip = req.ip;
const now = Date.now();
const arr = (hits.get(ip) || []).filter(t => now - t < WINDOW_MS);
arr.push(now);
hits.set(ip, arr);
if (arr.length > LIMIT) {
// отличаем спайк от общего наплыва: доля этого IP во всём трафике
const total = [...hits.values()].reduce((s, a) => s + a.length, 0);
const share = arr.length / total;
if (share > 0.5 && now - (lastNotify.get(ip) || 0) > NOTIFY_COOLDOWN) {
lastNotify.set(ip, now);
notify(
'🚦 Аномальный трафик',
`IP ${ip}: ${arr.length} req/min (${(share * 100).toFixed(0)}% всего трафика)\n` +
`Путь: ${req.method} ${req.path}`,
8
);
}
return res.status(429).json({error: 'rate limit'});
}
next();
}
app.use(rateGuard);

То же на FastAPI: middleware со скользящим окном и «долей клиента»:

import time
from collections import defaultdict, deque
from fastapi import FastAPI, Request
from fastapi.responses import JSONResponse
from notifly import notify
app = FastAPI()
WINDOW = 60 # окно в секундах
LIMIT = 300 # порог запросов от одного клиента
NOTIFY_COOLDOWN = 300
hits: dict[str, deque] = defaultdict(deque)
last_notify: dict[str, float] = defaultdict(float)
@app.middleware("http")
async def rate_guard(request: Request, call_next):
ip = request.client.host
now = time.time()
q = hits[ip]
q.append(now)
while q and now - q[0] > WINDOW:
q.popleft()
if len(q) > LIMIT:
total = sum(len(v) for v in hits.values())
share = len(q) / total if total else 0
# спайк одного клиента, а не общий наплыв
if share > 0.5 and now - last_notify[ip] > NOTIFY_COOLDOWN:
last_notify[ip] = now
notify(
"🚦 Аномальный трафик",
f"IP {ip}: {len(q)} req/min ({share*100:.0f}% всего трафика)\n"
f"Путь: {request.method} {request.url.path}",
8,
)
return JSONResponse({"error": "rate limit"}, status_code=429)
return await call_next(request)

Спайк против запуска: тихий сигнал о наплыве

Заголовок раздела «Спайк против запуска: тихий сигнал о наплыве»

Если трафик вырос, но размазан по многим адресам — это, скорее всего, хорошие новости (реклама сработала). Такое тоже полезно знать, но тихо:

# отдельная тихая проверка раз в минуту (например, из фоновой задачи)
def traffic_pulse(rpm: int, unique_ips: int):
if rpm > 1000 and unique_ips > 100:
# много запросов от многих людей — это наплыв, не абьюз
notify("📊 Наплыв трафика", f"{rpm} req/min от {unique_ips} IP — похоже, запуск зашёл", 4)
  • Много 401/403 подряд с одного IP — перебор паролей или токенов; приоритет 8.
  • Скрейпинг листингов. Последовательный обход ?page=1,2,3… быстрее человека.
  • Всплеск ошибок 5xx во время наплыва — сервису плохо; приоритет 9.
  • Порог и «здоровье» удобно вынести на HTTP-монитор.
  • Атаку видно на старте, а не по счёту за трафик в конце месяца.
  • Меньше ложных тревог. Доля клиента отличает реальный спайк от вашего же успешного запуска.
  • Тротлинг встроен — во время атаки телефон не звенит без остановки.