Фронтенд подписывает каждый 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 — его всё равно обходят, но стоимость скрейпинга растёт

Связанные заметки