Фронтенд подписывает каждый 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 | Ротация без пересборки | Чуть сложнее |
Ротация раз в сутки оптимальна — скрейпер должен каждый день заново извлекать секрет.
Ограничения
- JS открыт — определённый скрейпер может извлечь секрет через реверс-инжиниринг
- Убирает ~95% казуальных попыток (curl, Python requests, Scrapy)
- Аналогичный подход используют Airbnb, Zillow — его всё равно обходят, но стоимость скрейпинга растёт
Связанные заметки
- Защита API от скрейпинга — общая стратегия
- Поведенческий анализ трафика — следующий уровень защиты