Инженерная документация
Антибот-проверка форм без внешних сервисов. Подключается тегом script, отвечает как reCAPTCHA — чужой код часто уже умеет разбирать такой ответ. Что это и зачем — на главной.
Быстрый старт
Два шага. Скрипт сам находит блоки с классом kontur, решает задачу в фоне и подставляет в форму скрытое поле kontur-response. Ваш сервер отдаёт это поле нам вместе с секретом и получает «да» или «нет».
<script src="https://mallaev-dev.ru/kontur.js?k=knt_site_ваш_ключ" async defer></script> <form method="post" action="/submit"> <input name="email" type="email" required> <div class="kontur" data-sitekey="knt_site_ваш_ключ"></div> <button>Отправить</button> </form>
const res = await fetch('https://mallaev-dev.ru/api/kontur/v1/verify', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
secret: process.env.KONTUR_SECRET,
response: formData.get('kontur-response'),
}),
});
const { success } = await res.json();
if (!success) return new Response('Проверка не пройдена', { status: 400 });Ключи и домены
Kontur пока в начале пути, и каждое подключение я разбираю лично: смотрю, как проверка ведёт себя на конкретной форме, какие признаки поведения там вообще собираются и где она ошибается на живых людях. На чужой форме всё это выглядит иначе, чем на моей, а вслепую такие вещи не настраиваются. Поэтому ключи завожу я, и Kontur идёт вместе с сайтом, который я делаю.
knt_site_…Публичный. Стоит в адресе скрипта и в разметке, его видно всем — так же устроены reCAPTCHA и Turnstile. Без него /kontur.js отдаёт заглушку, а не проверку.
knt_secret_…Только для вашего сервера. Показывается один раз при выдаче, потом — в профиле по вводу текущего пароля. У нас он не хранится в открытом виде: в базе лежат хеш для поиска и шифртекст, ключ от которого не покидает Yandex KMS.
обязательноПока домены не указаны, проверка отклоняет все запросы. Впишите домен сайта, где стоит форма. Поддомены разрешённого адреса проходят, похожие имена — нет: evil-example.com не считается поддоменом example.com.
Выданное подключение лежит в разделе Kontur вашего профиля: там же домены, статистика по рубежам и перевыпуск секрета. Прежний секрет перестаёт работать сразу же, поэтому перевыпускать стоит только когда есть куда вписать новый.
Разметка и атрибуты
Блок должен стоять внутри формы — иначе скрытому полю некуда попасть, виджет выглядит рабочим, а проверка до сервера не доезжает. Скрипт скажет об этом в консоли один раз; значение тогда придётся забирать через getResponse().
data-sitekeyКлюч сайта. Можно не писать: скрипт берёт ключ из своего же адреса. Нужен, когда на странице несколько форм с разными ключами.
data-skinОформление: neutral (по умолчанию) подстраивается под светлую и тёмную тему вашего сайта, stand — фирменный тёмный вид Kontur.
data-themedark или light — закрепляет схему нейтрального оформления. Нужен сайтам, которые всегда тёмные или всегда светлые, независимо от темы системы посетителя.
data-callbackИмя глобальной функции. Вызывается со значением поля, когда пропуск готов. Нужно, если форму отправляет ваш код.
data-expired-callbackИмя глобальной функции. Вызывается, когда задача истекает и берётся новая — это происходит само, каждые 2 мин.
Проверка на сервере
Секрет никогда не должен попадать в браузер. У адреса проверки намеренно нет заголовков CORS: вызвать его со страницы не получится, и это сразу подскажет, что что-то не так.
{ "success": true, "hostname": "example.com", "challenge_ts": "2026-08-25T12:00:00Z" }
{ "success": false, "error-codes": ["invalid-response"] }При отказе с кодом challenge-required в ответе приходит поле challenge с готовой картинкой. Верните его на страницу и передайте в виджет — иначе посетитель, ошибшийся один раз, упрётся в тупик до перезагрузки.
const data = await res.json();
if (!data.success) {
if (data.challenge) window.kontur.challenge(data.challenge);
return;
}JavaScript API
Аргумент target есть у всех методов, работающих с одной проверкой: индекс, id блока или сам элемент. Он нужен, когда форм на странице больше одной — без него берётся первая, и скрипт предупредит об этом в консоли.
kontur.render(scope?)Ищет непроинициализированные блоки .kontur и поднимает их. Нужен, когда форма появилась после загрузки страницы — в модальном окне или после перерисовки.
kontur.getResponse(target?)Значение скрытого поля. Нужен, если форму отправляет ваш код, а не браузер.
kontur.reset(target?)Берёт новую задачу. Без аргумента сбрасывает все проверки на странице.
kontur.challenge(payload, target?)Показывает картинку, которую вернул ваш /verify в поле challenge. Без этого посетитель, ошибшийся один раз, упирался в тупик до перезагрузки.
<div class="kontur" id="signup-check" data-callback="onKonturReady"></div>
<script>
function onKonturReady(response) {
// пропуск готов — можно отправлять форму своим кодом
}
// на странице с несколькими формами называйте нужную явно
const value = window.kontur.getResponse('signup-check');
</script>Коды ошибок
missing-input-secretВ запросе нет поля secretinvalid-input-secretСекрет не найден или подключение выключеноmissing-input-responseВ запросе нет поля responseinvalid-responseОтвет повреждён, просрочен или не проходит проверкуchallenge-requiredНужна картинка: показать её человеку и повторить отправкуinvalid-site-keyКлюч сайта не найден или подключение выключено. Проверьте на /kontur/check.domain-not-allowedДомен страницы не вписан в список разрешённых у этого ключа.exchange-unavailableОбмен картинки на задачу не состоялся: токен уже погашен или просрочен.Проверка подключения
Выдача задачи отвечает одинаковым отказом и на неверный ключ, и на выключенное подключение, и на чужой домен — намеренно: по разнице ответов подбирались бы чужие ключи. Отлаживать по такому ответу нечего, поэтому три случая разделены на отдельной странице.
Проверить подключениеЖивая проверка
Кнопка показывает второй режим — так проверка выглядит, когда отправка кажется подозрительной.
Это не картинка и не запись — рабочий виджет, ровно тот же, что встаёт на форму подключённого сайта. Он уже проверил вас, пока вы читали эту строку.
Как считается риск
Четыре отправки одной и той же формы, посчитанные настоящим движком. Признаки браузера умеют только снижать подозрительность, и их суммы хватает ровно до обычного режима; повышать без ограничения умеют лишь серверные факты — время, происхождение адреса, форма заголовков, номер попытки.
Заполнил форму мышью и клавиатурой, с домашнего адреса.
- -8Страницу трогали
- -7Неровный ритм набора
- -6Живая траектория курсора
- -5Пауза перед первым полем
Считает настоящий движок — те же веса, что применяются к вашей отправке. Минус снижает подозрительность, плюс повышает: признаки браузера умеют только снижать, и их суммы хватает ровно до обычного режима.
Сравнение
Строки выбраны по тому, что решает при выборе, а не по тому, где Kontur выигрывает. Последняя строка — там, где он проигрывает, и её стоит прочитать раньше остальных.
| Параметр | Kontur | reCAPTCHA | Turnstile |
|---|---|---|---|
| Кто видит посетителя | Только ваш сервер и mallaev-dev.ru | Google — и связывает визит с аккаунтом | Cloudflare |
| Что делает человек | Обычно ничего: задача решается в фоне | Обычно ничего, иногда — светофоры | Обычно ничего |
| Если картинку не видно | Задача вместо неё, по одному вычислению | Аудио — и оно распознаётся ботом | Картинки нет вовсе |
| Согласие на обработку | Не нужно: данные не уходят третьим лицам | Нужно упомянуть в политике | Нужно упомянуть в политике |
| Кто чинит, если сломалось | Я, и вы знаете, кому писать | Никто персонально | Никто персонально |
| Устоит против выделенной атаки | Нет — задача решается на видеокарте | Тоже нет, но моделей у них больше | Тоже нет |
Что нового в 2.0
Обновление про одно: раньше браузер рассказывал о себе, а сервер верил. Теперь браузер показывает, а сервер проверяет — и повышать подозрительность стали факты, которые с клиента подделать нельзя. Для живого посетителя не изменилось ничего.
- 01
Поведение проверяется, а не принимается на слово
Раньше браузер присылал готовые выводы о себе: «страницу трогали», «ритм набора такой-то». Сервер их принимал. Бот, написавший эти шесть чисел от руки, проходил незаметно — и это был самый дешёвый обход, какой в Kontur был. Теперь браузер присылает саму последовательность событий, а выводы делает сервер. Их можно сверить: поведение не может длиться дольше, чем прошло от выдачи задачи, а это время сервер отмерил сам.
- 02
Отсекаются арендованные адреса
Около полутора тысяч диапазонов AWS, Google Cloud, Oracle, DigitalOcean, Linode и Vultr. Оттуда приходит почти вся дешёвая автоматика, и оттуда отправка теперь получает картинку. Сети доставки содержимого в список не входят: через них выходят живые люди — например, все, у кого включено «Частное реле iCloud».
- 03
Заметен клиент, который только притворяется браузером
Браузер отправляет набор заголовков целиком и всегда одинаково. Подделать одно поле легко, собрать весь набор — заметно труднее, а забыть половину очень просто, и именно это делают простые скрипты.
- 04
Картинка появляется там, где раньше не могла
В подписке на рассылку и восстановлении пароля её показать было негде — не по существу, а по устройству виджета. Теперь есть где, и подозрительная отправка там встречает визуальный рубеж, а не просто задачу подороже.
- 05
Постоянный посетитель платит меньше
Браузер, прошедший несколько проверок за сутки, получает задачу вчетверо легче. Память привязана к браузеру, а не к адресу, поэтому переход с домашнего Wi-Fi на мобильный интернет её не сбрасывает. На чужих сайтах это не работает: там это кука третьей стороны, и браузеры её режут.
- 06
Вам делать не нужно ничего
Скрипт обновляется сам, ключ и секрет прежние, ответ /verify не менялся ни одним полем. Единственное, что стоит знать: первые минут десять после обновления у части посетителей в кеше ещё лежит старый скрипт, и они увидят картинку. Проходит само.
Чего Kontur не делает
Ключ сайта публичен по определению — он лежит в разметке, его видно всем. От чужого использования его защищает список разрешённых доменов, и защищает лишь в браузере: заголовок с адресом страницы браузер проставляет сам и не даёт его подделать, а запрос не из браузера может прислать любой. Так же устроены reCAPTCHA и Turnstile — настоящий барьер даёт не привязка к домену, а вычислительная задача: она стоит одинаково при любом заголовке.
Выдача скрипта по ключу — не защита от копирования. Она закрывает копипасту тега с чужой страницы и вызов без ключа, но браузер вашего посетителя всё равно получает исходный текст скрипта — иначе он не смог бы его исполнить. Ценность не в нём: движок риска, оценка поведения, отрисовка картинки и проверка вычисления живут на сервере и наружу не уезжают.
Поведенческие признаки подделываются целенаправленным ботом. Они отсекают дешёвую автоматизацию, не более.
Объёмный DDoS гасит только хостинг. Наплыв запросов по HTTP на mallaev-dev.ru сдерживает режим «под атакой»: проверка стоит в nginx, до приложения, и бот без пропуска до сервера сайта не доходит. Но атака, забивающая сам канал, до nginx не доберётся вовсе — её гасит инфраструктура хостинга. Подключаемым сайтам режим пока не предлагается: там Kontur защищает формы — от рассылки, перебора паролей и слива бюджета на языковые модели, — а не входящий трафик.
Внешний сервис в схеме всё-таки есть — Yandex KMS, но не на пути проверки. Им зашифрован ваш секрет, чтобы его можно было показать вам второй раз, не храня открытым. Проверка отправки формы ищет сайт по хешу и в KMS не заходит никогда, поэтому недоступность облака её не ломает.
Ключа ещё нет? Расскажите о задаче — подключение завожу я, и оно появляется в вашем профиле.