Фронтенд подписывает каждый API-запрос секретным ключом. Сервер проверяет подпись — без неё 403. Не криптографическая защита, а повышение порога входа для скрейперов.
Поток запроса
1. SPA загружается → получает секрет (в бандле или с сервера)
2. Перед каждым fetch:
signature = HMAC-SHA256(timestamp + method + path, secret)
3. Запрос уходит с заголовками:
X-Timestamp: 1711800000
X-Signature: a3f2b8c1d4...
4. Сервер:
— вычисляет HMAC тем же секретом
— сравнивает с X-Signature
— проверяет timestamp не старше 5 минут (anti-replay)
— не совпало → 403
Реализация
~30 строк кода: одна функция-обёртка на фронтенде, один хук на бэкенде.
Фронтенд — обёртка над fetch:
async function apiFetch(method, path, body) {
const timestamp = Math.floor(Date.now() / 1000)
const payload = `${timestamp}:${method}:${path}`
const signature = await hmacSHA256(payload, API_SECRET)
return fetch(path, {
method,
headers: { 'X-Timestamp': timestamp, 'X-Signature': signature },
body,
})
}Бэкенд — Fastify/Express хук:
app.addHook('onRequest', (req, reply, done) => {
const sig = req.headers['x-signature']
const ts = req.headers['x-timestamp']
if (Math.abs(Date.now()/1000 - ts) > 300) return reply.code(403).send()
const expected = hmacSHA256(`${ts}:${req.method}:${req.url}`, SECRET)
if (sig !== expected) return reply.code(403).send()
done()
})Доставка секрета
| Вариант | Плюсы | Минусы |
|---|---|---|
| Зашить в бандл (env при сборке) | Просто | Менять = пересобирать |
| Отдельный эндпоинт при загрузке SPA | Ротация без пересборки | Чуть сложнее |
Ротация раз в сутки оптимальна — скрейпер должен каждый день заново извлекать секрет.
Suspicion score вместо жёсткого 403
Альтернативный подход: отсутствие или невалидная подпись не блокирует запрос, а повышает suspicion score. Преимущества:
- Honeypot-эндпоинты остаются доступными для скраперов (иначе ловушка не работает)
- Нет жёсткого breakpoint — скрапер не получает однозначный сигнал «меня обнаружили»
- Комбинируется с другими сигналами (поведенческий анализ, TLS fingerprint)
Невалидная подпись → suspicion += 50
Нет подписи вообще → suspicion += 30
Невалидный timestamp → suspicion += 30
При suspicion > 80 → tarpit или блок.
Ротация ключа
| Стратегия | Частота | Сложность |
|---|---|---|
| При каждом деплое (env variable) | Зависит от частоты деплоев | Минимальная |
По расписанию (эндпоинт /api/app-config) | Каждые 24ч | Средняя — нужен механизм получения нового ключа |
| Per-session | Уникальный ключ каждой сессии | Высокая — нужен key exchange |
Оптимально для начала: ключ при деплое. Если деплой раз в неделю — скрапер должен обновлять ключ раз в неделю.
Ограничения
- JS открыт — определённый скрейпер может извлечь секрет через реверс-инжиниринг. Статья Reverse Engineering Claude Code’s Request Signing описывает как был реверсирован HMAC даже из скомпилированного Zig-кода
- Убирает ~90% казуальных попыток (curl, Python requests, Scrapy)
- Обфускация JS замедляет, но не останавливает. WASM/native код — сложнее, но тоже реверсируется через memory analysis
- Поэтому HMAC — soft signal для suspicion score, а не блокирующий фактор
- Ротация ключа раз в неделю/месяц дополнительно усложняет жизнь скрейперам
Связанные заметки
- Защита API от скрейпинга — общая стратегия
- Поведенческий анализ трафика — следующий уровень защиты