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

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

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

Сайт при этом может оставаться где угодно — переводить домен на 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",
}

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

Одно условие: это работает только для тех, кого вы передавали при скоринге — и с тем же секретом. Собственных данных о ваших пользователях у нас нет и быть не должно, связывать больше нечем. Если идентификатор незнаком, ответ придёт с "matched": 0 — это не ошибка. В matched всегда видно, сколько сессий размечено, а в sessions — какие именно.

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

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

Отказ (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: тег запрашивает сессию по самому нажатию «Отправить», а не по фоновому таймеру, поэтому поле заполнено и у того, кто заполнил форму за пару секунд.

Идентификатор сессии знает только страница, и прочитать его на бэкенде неоткуда: тег работает кросс-доменно и намеренно не ставит cookie. Значение wsAf.state().sid появляется не сразу — сессию выдаёт наш коллектор в ответ на первое событие, а тег перед его отправкой некоторое время наблюдает за посетителем. Поэтому не ждите фоновый таймер, а запросите сессию при первом касании формы:

// Тег загружается асинхронно, поэтому проверяем, что он уже есть.
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=…) — так запрос приходит на наш адрес напрямую и мы видим сетевые признаки соединения.

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

Потребление проекта (события тега и вызовы скоринга) считается с первого дня и видно в карточке проекта.