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

Защита форм и регистраций

Защита от фейковых регистраций, спама через формы и накрутки: установка тега, скоринг события, расшифровка вердиктов. Работает и для внешних сайтов, и для сайтов за WebShield.

Защита форм отвечает на вопрос «можно ли доверять этому конкретному действию»: регистрации, входу, отправке формы. Вы ставите на страницы небольшой тег, ваш бэкенд в нужный момент спрашивает у нас оценку и получает вердикт с причинами.

Сайт при этом может оставаться где угодно — переводить домен на WebShield не нужно. Если он уже за нашей защитой, защита форм всё равно ставится отдельно и решает свою задачу: см. Если сайт уже за WebShield.

  1. Создайте проект — получите ключ сайта.
  2. Поставьте тег на страницы и перечислите в нём формы.
  3. Вызовите скоринг из своего бэкенда и примените вердикт.

Дальше по желанию: сообщать нам исход заявки, чтобы оценка подстраивалась под ваш сайт.

В личном кабинете откройте раздел Услуги → Защита форм и создайте проект:

  • Название — произвольное, для вашего удобства.
  • Разрешённые адреса сайта — схема и домен без пути, например https://example.com и https://www.example.com. События принимаются только с этих адресов.

Вы получите ключ сайта и готовый код для вставки.

Разместите код в <head> на всех страницах, включая страницы с формами. Если формы есть — перечислите их через запятую в атрибуте data-forms обычными CSS-селекторами:

<script src="https://af.webshield.pro/t/v1.js?k=ВАШ_КЛЮЧ"
data-forms="#signup, #contact_form" async></script>

Тег ничего не блокирует и не задерживает загрузку: атрибут async обязателен.

Место в <head> предпочтительнее конца <body> по двум причинам. Во-первых, работа посетителя со страницей отсчитывается с момента запуска тега: поздний старт искажает эту картину не в пользу обычного посетителя. Во-вторых, тег раньше готов к работе — меньше шансов, что форму отправят до того, как он её привяжет. Разметка на этот момент может быть ещё не готова, и это нормально: тег дождётся её сам.

Ключ сайта публичный — он виден в исходном коде страницы, и это нормально: доступ ограничивает список разрешённых адресов.

Что тег сделает с перечисленными формами:

  1. подставит в каждую скрытое поле ws_sid — идентификатор сессии, который нужен вашему бэкенду;
  2. на отправке спросит у нас вердикт по этому действию;
  3. при сомнительном вердикте покажет посетителю проверку и продолжит отправку после решения — если проверка включена.

Имя скрытого поля меняется атрибутом data-sid-field="имя". Селектор, который на странице ни во что не попал, просто не сработает. Формы, появившиеся позже — модальные окна, SPA-роутинг — тег подхватывает сам, следя за изменениями разметки; делать для этого ничего не нужно.

Если форму отправляет ваш собственный код и вы хотите управлять моментом отправки сами — см. Ручная привязка формы.

Понадобится API-токен со скоупом antifraud:write. Бэкенд принимает поле ws_sid из формы и передаёт его как session_id:

import logging
import requests
def ws_decision(session_id: str, event: str, identity: str = "") -> str:
"""Вердикт WebShield: "allow", "challenge" или "deny"."""
try:
resp = requests.post(
"https://webshield.pro/api/v1/antifraud/score",
headers={"Authorization": f"Bearer {WS_TOKEN}"},
json={
"site_key": WS_SITE_KEY,
"session_id": session_id,
"event": event,
# Необязательно, см. «Идентификатор пользователя» ниже.
"identity_sha256": identity,
},
timeout=3,
)
resp.raise_for_status()
return resp.json()["decision"]
except (requests.RequestException, ValueError, KeyError):
logging.exception("WebShield score failed")
return "allow"
decision = ws_decision(request.POST.get("ws_sid", ""), "signup", ws_identity(email))
if decision == "deny":
... # отказать
elif decision == "challenge":
... # попросить пройти проверку (см. «Проверка посетителя») и вызвать скоринг ещё раз
else:
... # принять действие

Ответ:

{
"decision": "challenge",
"score": 55,
"reasons": ["behavior_auto", "velocity_fp"],
"needs_challenge": true,
"session": { "visitor_id": "…", "device_id_hw": "…", "behavior": "auto" }
}

Несколько практических правил:

  • Действие принимайте только при allow. Остальные два вердикта требуют вашего решения, и оба важны: deny — отказ, challenge — проверка вместо отказа.
  • Пустой или неизвестный session_id — не ошибка, но и не «чисто». Ответ придёт с причиной no_session и вердиктом challenge. Так выглядит и посетитель без JavaScript, и запрос, посланный вообще мимо браузера — а это самый дешёвый обход, и оставлять его без внимания нельзя. Вызов пропускать не нужно: отсутствие сессии само по себе признак.
  • event придумываете вы — signup, login, contact. Имена стоит давать разные: скорость событий считается отдельно по каждому, и десять регистраций в час не должны смешиваться с десятью сообщениями.
  • Сессия одна на посетителя, а не на форму. Она переживает перезагрузки страницы, поэтому один sid покрывает и открытие формы, и отправку, и повторную попытку после ошибки валидации.

Необязательное поле, заметно повышающее качество оценки. Передавайте его, если у вас есть идентификатор пользователя: адрес почты, номер телефона, sub из вашего SSO (Google, Apple, Keycloak) или внутренний идентификатор аккаунта — подойдёт любой, лишь бы у одного человека он был один и тот же.

Сырых адресов, телефонов и имён мы не принимаем — только хеш, поэтому данные ваших пользователей в нашу систему не попадают.

Считайте его так:

import hmac
import os
# Ваш собственный секрет — случайная строка, созданная ОДИН раз:
# python -c "import secrets; print(secrets.token_hex(32))"
# Храните её вместе с остальными секретами приложения и больше не меняйте: со
# сменой секрета один и тот же человек станет для нас другим. Нам её не передают.
WS_SALT = os.environ["WS_SALT"].encode() # ключ HMAC — байты, не строка
def ws_identity(value: str) -> str:
return hmac.new(WS_SALT, value.strip().lower().encode(), "sha256").hexdigest()

Два условия, чтобы связывание работало:

  • приводите значение к единому виду — телефон в формате E.164 (+79991234567), почту в нижнем регистре без пробелов по краям (это делает ws_identity выше). Иначе Ivan@example.com и ivan@example.com окажутся для нас разными людьми;
  • используйте один и тот же секрет в скоринге и в отзыве об исходе — иначе мы не свяжем одно с другим.

Чем закончилась заявка, знаете только вы: чарджбэк, забаненный аккаунт, спам в форме — или, наоборот, состоявшаяся покупка. Присылайте исход, когда он станет известен, хоть через неделю:

requests.post(
"https://webshield.pro/api/v1/antifraud/feedback",
headers={"Authorization": f"Bearer {WS_TOKEN}"},
json={
"site_key": WS_SITE_KEY,
"session_id": saved_ws_sid, # тот же, что был в скоринге
"event": "signup",
"outcome": "fraud", # или "legit"
},
timeout=3,
)

Зачем это вам:

  • устройство и адрес, помеченные как fraud, дают дополнительный вес в оценке;
  • ошибку можно исправить: legit по тому же действию снимает метку;
  • накопленные исходы — единственный способ настроить пороги под ваш сайт, а не под средний.

Повторный отзыв о том же действии обновляет прежний, а не добавляет новый.

Хранить идентификатор сессии у себя удобно не всем — это лишнее поле в базе. Тогда присылайте вместо него identity_sha256: идентификатор пользователя у вас есть всегда.

json={
"site_key": WS_SITE_KEY,
"identity_sha256": ws_identity(email), # тот же секрет, что и в скоринге
"event": "signup",
"outcome": "fraud",
}

Мы найдём по нему последние сессии этого пользователя и разметим их — то есть отзыв по-прежнему привязывается к конкретным действиям, идентификатор лишь помогает их найти.

Явный session_id точнее: он указывает на одно конкретное действие, тогда как по идентификатору помечаются несколько последних сессий. Если оба поля переданы, используется session_id.

Вердикт Смысл Что обычно делают
allow Признаков нарушения не найдено Пропустить
challenge Признаки есть, но их недостаточно для отказа — либо наблюдений нет вовсе (no_session) Показать проверку, подтверждение по почте, отложенную модерацию
deny Сильные признаки, подтверждённые нашими данными Отклонить или отправить на ручную проверку

Блок session — то, что мы видели в этой сессии:

Поле Смысл
visitor_id посетитель: переживает перезагрузки страницы и переходы по сайту
device_id, device_id_hw идентификаторы устройства
ip адрес посетителя
behavior как выглядела работа со страницей: human, weak, auto, idle
tls_class, hdr_class похожи ли соединение и заголовки на настоящий браузер
pages, visits, events, submits, age_seconds структура визита: сколько страниц открыли, сколько заходов за неделю, сколько было событий и отправок, сколько идёт сессия

Когда оценка неоднозначна, в ответе приходит "needs_challenge": true — предложение показать посетителю короткую задачу с картинками, которую рисует наш тег поверх вашей страницы. Это выход для человека, которого мы не смогли уверенно отнести к людям; доказательством «я не бот» решённая задача не является.

Если формы перечислены в data-forms, показ уже встроен — делать ничего не нужно. Для своего кода:

if (resp.needs_challenge) {
try {
await wsAf.challenge(); // покажет задачу и дождётся решения
await submitAgain(); // тот же запрос, тот же ws_sid
} catch (err) {
// Посетитель закрыл проверку или не решил её.
}
}

Повторный вызов скоринга идёт с тем же session_id — отметку о решённой задаче мы храним у себя.

Решённая задача снижает оценку, но не отменяет улик и действует на несколько ближайших вызовов, а не до конца сессии. Если она не изменила вердикт, в reasons придёт captcha_passed_but_denied или captcha_passed_but_auto — по ним видно, что задача была решена, но подозрения остались.

Единственный переключатель проекта отвечает ровно за одно: может ли тег прервать посетителя проверкой. По умолчанию выключен — посетитель ничего не видит.

На вердикт в API это не влияет. decision, score и reasons настоящие в обоих положениях. Отличается только needs_challenge: при выключенной проверке он всегда false, потому что показывать нечего.

Так и стоит начинать: проверка выключена, несколько дней логируете вердикты рядом с исходом заявки, смотрите отчёт — и только потом включаете проверку и начинаете действовать по decision.

В карточке проекта доступен отчёт:

  • Автоматизация — доля событий с признаками автоматизированного браузера.
  • Устройства — число различимых устройств; сравните с числом посетителей.
  • Причины срабатываний — что именно чаще всего срабатывает.
  • Подозрение на ферму — одно устройство заходит с нескольких адресов.
  • Последние сессии — визиты по одному: время, адрес, страницы, отправки формы, поведение, оценка.

Спорный случай удобнее разбирать по сессиям: сводка отвечает «сколько», а решение принимается по конкретному визиту.

Нужна, только если форму отправляет ваш код и вы хотите управлять моментом отправки сами. Во всех остальных случаях лучше перечислить формы в data-forms: тег запрашивает сессию по самому нажатию «Отправить», а не по фоновому таймеру, поэтому поле заполнено и у того, кто заполнил форму за пару секунд.

// Тег загружается асинхронно, поэтому проверяем, что он уже есть.
document.querySelector("#signup").addEventListener("focusin", function () {
if (window.wsAf) wsAf.event("form_open");
}, { once: true });
// Отправка через JavaScript: берите значение прямо перед запросом, а не заранее.
payload.ws_sid = (window.wsAf && wsAf.state().sid) || "";

Для обычной формы с перезагрузкой страницы заполняйте скрытое поле, как только сессия получена:

<form id="signup" method="post" action="/signup">
<input type="hidden" name="ws_sid" value="">
</form>
<script>
window.addEventListener("load", function () {
if (!window.wsAf) return;
wsAf.onVerdict(function (state) {
var field = document.querySelector('#signup input[name="ws_sid"]');
if (field) field.value = state.sid || "";
});
});
</script>

Из кода страницы доступны также wsAf.token() (текущий токен сессии), wsAf.event("имя") (своё событие) и wsAf.onVerdict(fn) (вердикт, когда он придёт).

  • Привязка форм в теге — удобство, а не защита. Решение принимается в браузере, а бот просто не выполнит наш обработчик и отправит запрос напрямую.
  • Бот без JavaScript тега не выполнит — это видно как no_session, и вердиктом по умолчанию будет challenge: наблюдений у нас в этом случае нет вовсе, а пропускать запрос, посланный мимо браузера, нельзя. Для мобильного приложения, которое ходит в тот же API без страницы, это ожидаемо — заводите под него отдельный путь.

Защита домена и защита форм решают разные задачи. Защита от ботов работает на уровне запроса: пропустить, показать проверку браузера, заблокировать.

Защита форм отвечает именно на вопрос «принимать ли эту заявку» и делает это в момент, когда решение принимает ваш код. Поэтому её проект нужен даже когда сайт уже проксируется через нас. Порядок подключения тот же; отличий два:

  • в разрешённых адресах укажите домен, защищённый у нас;
  • в оценку дополнительно попадает репутация адреса, накопленная нашей защитой на вашем же домене: посетитель, ранее пойманный на переборе или входящий в выявленную ферму, приходит уже с этим признаком.

Тег остаётся кросс-доменным (https://af.webshield.pro/t/v1.js?k=…) — так запрос приходит на наш адрес напрямую и мы видим сетевые признаки соединения.

Если задача — только видеть автоматизацию в статистике, проект не нужен: хватит скрипта на сайте. Ставить оба скрипта сразу не нужно.