Защита форм и регистраций
Защита форм отвечает на вопрос «можно ли доверять этому конкретному действию»: регистрации, входу, отправке формы. Вы ставите на страницы небольшой тег, ваш бэкенд в нужный момент спрашивает у нас оценку и получает вердикт с причинами.
Сайт при этом может оставаться где угодно — переводить домен на WebShield не нужно. Если он уже за нашей защитой, защита форм всё равно ставится отдельно и решает свою задачу: см. Если сайт уже за WebShield.
Подключение за три шага
Заголовок раздела «Подключение за три шага»- Создайте проект — получите ключ сайта.
- Поставьте тег на страницы и перечислите в нём формы.
- Вызовите скоринг из своего бэкенда и примените вердикт.
Дальше по желанию: сообщать нам исход заявки, чтобы оценка подстраивалась под ваш сайт.
Шаг 1. Создайте проект
Заголовок раздела «Шаг 1. Создайте проект»В личном кабинете откройте раздел Услуги → Защита форм и создайте проект:
- Название — произвольное, для вашего удобства.
- Разрешённые адреса сайта — схема и домен без пути, например
https://example.comиhttps://www.example.com. События принимаются только с этих адресов.
Вы получите ключ сайта и готовый код для вставки.
Шаг 2. Поставьте тег
Заголовок раздела «Шаг 2. Поставьте тег»Разместите код в <head> на всех страницах, включая страницы с формами. Если формы есть — перечислите их
через запятую в атрибуте data-forms обычными CSS-селекторами:
<script src="https://af.webshield.pro/t/v1.js?k=ВАШ_КЛЮЧ" data-forms="#signup, #contact_form" async></script>Тег ничего не блокирует и не задерживает загрузку: атрибут async обязателен, а тяжёлая часть работы
(слепок окружения) выполняется не при загрузке, а позже — вместе с первым событием.
Место в <head> предпочтительнее конца <body> по двум причинам. Во-первых, работа посетителя со
страницей отсчитывается с момента запуска тега: поздний старт искажает эту картину не в пользу обычного
посетителя. Во-вторых, тег раньше готов к работе — меньше шансов, что форму отправят до того, как он её
привяжет. Разметка на этот момент может быть ещё не готова, и это нормально: тег дождётся её сам.
Ключ сайта публичный — он виден в исходном коде страницы, и это нормально: доступ ограничивает список разрешённых адресов. Ссылка на скрипт постоянная, обновления приезжают сами.
Что тег сделает с перечисленными формами:
- подставит в каждую скрытое поле
ws_sid— идентификатор сессии, который нужен вашему бэкенду; - на отправке спросит у нас вердикт по этому действию;
- при сомнительном вердикте покажет посетителю проверку и продолжит отправку после решения — если проверка включена.
Имя скрытого поля меняется атрибутом data-sid-field="имя". Селектор, который на странице ни во что не
попал, просто не сработает. Формы, появившиеся позже — модальные окна, SPA-роутинг, — тег подхватывает
сам, следя за изменениями разметки; делать для этого ничего не нужно.
Если форму отправляет ваш собственный код и вы хотите управлять моментом отправки сами — см. Ручная привязка формы.
Шаг 3. Вызовите скоринг из бэкенда
Заголовок раздела «Шаг 3. Вызовите скоринг из бэкенда»Понадобится 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покрывает и открытие формы, и отправку, и повторную попытку после ошибки валидации.
Идентификатор пользователя: identity_sha256
Заголовок раздела «Идентификатор пользователя: identity_sha256»Необязательное поле, заметно повышающее качество оценки. Передавайте его, если у вас есть идентификатор
пользователя: адрес почты, номер телефона, sub из вашего SSO (Google, Apple, Keycloak) или внутренний
идентификатор аккаунта — подойдёт любой, лишь бы у одного человека он был один и тот же.
Сырых адресов, телефонов и имён мы не принимаем принципиально — только хеш, поэтому данные ваших пользователей в нашу систему не попадают.
Считайте его так:
import hmacimport 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окажутся для нас разными людьми; - используйте один и тот же секрет в скоринге и в отзыве об исходе — иначе мы не свяжем одно с другим.
Шаг 4 (необязательный). Сообщайте исход
Заголовок раздела «Шаг 4 (необязательный). Сообщайте исход»Чем закончилась заявка, знаете только вы: чарджбэк, забаненный аккаунт, спам в форме — или, наоборот, состоявшаяся покупка. Присылайте исход, когда он станет известен, хоть через неделю:
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по тому же действию снимает метку; - накопленные исходы — единственный способ настроить пороги под ваш сайт, а не под средний.
Повторный отзыв о том же действии обновляет прежний, а не добавляет новый. Свободного текста в запросе нет намеренно: для настройки достаточно самого исхода.
Если session_id не сохранён
Заголовок раздела «Если session_id не сохранён»Хранить идентификатор сессии у себя удобно не всем — это лишнее поле в базе. Тогда присылайте вместо него
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 без страницы, это ожидаемо — заводите под него отдельный путь, не полагающийся на тег. - Оценка вероятностная. Ни одна система не выявляет автоматизацию со стопроцентной точностью, и автоматизация настоящего браузера — самый трудный для распознавания случай.
- Данные из вашего периметра не влияют на других клиентов платформы — мы принимаем их только для ваших оценок и отчётов.
Что собирается
Заголовок раздела «Что собирается»Технический слепок окружения (параметры экрана, графической подсистемы, набор шрифтов), обезличенные агрегаты поведения, адрес, заголовки и параметры соединения. Не собираются: содержимое полей форм, текст страницы, нажатые символы, контактные данные.
По форме считаются только обезличенные счётчики и тайминги работы с ней. Какие именно символы набраны, мы не знаем и не сохраняем.
Если сайт уже за WebShield
Заголовок раздела «Если сайт уже за WebShield»Защита домена и защита форм решают разные задачи. Защита от ботов работает на уровне запроса: пропустить, показать проверку браузера, заблокировать. О смысле действия она ничего не знает — для неё отправка формы регистрации неотличима от открытия страницы.
Защита форм отвечает именно на вопрос «принимать ли эту заявку» и делает это в момент, когда решение принимает ваш код. Поэтому её проект нужен даже когда сайт уже проксируется через нас. Порядок подключения тот же; отличий два:
- в разрешённых адресах укажите домен, защищённый у нас;
- в оценку дополнительно попадает репутация адреса, накопленная нашей защитой на вашем же домене: посетитель, ранее пойманный на переборе или входящий в выявленную ферму, приходит уже с этим признаком.
Тег остаётся кросс-доменным (https://af.webshield.pro/t/v1.js?k=…) — так запрос приходит на наш адрес
напрямую и мы видим сетевые признаки соединения.
Если задача — только видеть автоматизацию в статистике, проект не нужен: хватит скрипта на сайте. Ставить оба скрипта сразу не нужно — тег защиты форм собирает всё то же самое.
Потребление проекта (события тега и вызовы скоринга) считается с первого дня и видно в карточке проекта.