Подключение Майли Защиты
От первого виджета до проверки на вашем сервере.
1. Создайте подключение
- Войдите через ваш аккаунт Майли и откройте «Мои сайты».
- Укажите название сайта и разрешённые домены. Каждый поддомен добавляется отдельно.
- Сохраните публичный и секретный ключи. Секрет показан только один раз.
Пример: example.ru и www.example.ru — два отдельных разрешённых адреса. Кириллица поддерживается и преобразуется в IDNA. Виджет работает на HTTPS-доменах с портом 443; маски доменов, IP-адреса и localhost не поддерживаются в этой версии.
Публичный ключ начинается с mc_ и используется в HTML. Секретный ключ начинается с ms_ и хранится только на вашем сервере в переменной окружения MAILI_CAPTCHA_SECRET. Смена секрета, приостановка и редактирование доменов отзывают ранее выданные токены.
2. Добавьте виджет в форму
Подключите скрипт один раз на странице и разместите блок внутри формы. Замените ВАШ_ПУБЛИЧНЫЙ_КЛЮЧ на ключ из кабинета.
<script src="https://xn--80aatf4czb.xn--80aqcic.xn--p1ai/v1/widget.js" async defer></script>
<form action="/signup" method="post">
<!-- Ваш CSRF-токен и поля регистрации -->
<input type="email" name="email" required>
<div class="maili-captcha"
data-sitekey="ВАШ_ПУБЛИЧНЫЙ_КЛЮЧ"
data-action="signup"></div>
<button type="submit">Зарегистрироваться</button>
</form>После проверки виджет создаёт скрытое поле maili-captcha-response с одноразовым токеном. Вместе с другими полями формы оно отправляется вашему серверу. Не выполняйте регистрацию, вход или отправку сообщения до серверной проверки.
data-action: от 1 до 32 латинских букв, цифр, _ и -. Например: signup, login, contact. Значение должно точно совпадать с ожидаемым действием на сервере.
3. Проверьте токен на сервере
POSThttps://xn--80aatf4czb.xn--80aqcic.xn--p1ai/v1/siteverifyAPI принимает JSON или application/x-www-form-urlencoded. Секрет передаётся в теле POST, а не в URL. Заголовок Content-Type обязателен. Максимальный размер тела — 4 КБ.
| Параметр | Назначение |
|---|---|
secret | Секретный ключ этого сайта. Обязательно. |
response | Токен из формы. Обязательно. |
hostname | Ожидаемый домен, например example.ru. Задайте на сервере; не берите из формы пользователя. Обязательно. |
action | Ожидаемое действие, например signup. Задайте на сервере. Обязательно. |
remoteip | IP посетителя. Необязательно; передавайте только достоверный IP из вашей доверенной прокси-цепочки. При несовпадении токен отклоняется. |
Python · стандартная библиотека
import json, os
from urllib.request import Request, urlopen
from urllib.error import URLError
def captcha_valid(token):
body = json.dumps({
"secret": os.environ["MAILI_CAPTCHA_SECRET"],
"response": token,
"hostname": "example.ru", # ожидаемый домен вашего сайта
"action": "signup", # ожидаемое действие этой формы
}).encode()
request = Request(
"https://xn--80aatf4czb.xn--80aqcic.xn--p1ai/v1/siteverify",
data=body, headers={"Content-Type": "application/json"},
method="POST",
)
try:
with urlopen(request, timeout=5) as response:
result = json.load(response)
return (isinstance(result, dict)
and result.get("success") is True
and result.get("hostname") == "example.ru"
and result.get("action") == "signup")
except (URLError, TimeoutError, ValueError):
return False
# token = request.POST.get("maili-captcha-response", "")
# if not captcha_valid(token): отклонить запрос и обновить виджет
# Только после успешной проверки выполнять регистрацию.Node.js · fetch
async function captchaValid(token) {
try {
const response = await fetch(
"https://xn--80aatf4czb.xn--80aqcic.xn--p1ai/v1/siteverify",
{
method: "POST",
headers: {"Content-Type": "application/json"},
body: JSON.stringify({
secret: process.env.MAILI_CAPTCHA_SECRET,
response: token,
hostname: "example.ru",
action: "signup"
}),
signal: AbortSignal.timeout(5000)
}
);
if (!response.ok) return false;
const result = await response.json();
return result.success === true
&& result.hostname === "example.ru"
&& result.action === "signup";
} catch { return false; }
}
// token = req.body["maili-captcha-response"];
// При false не выполнять действие. Запросить новую проверку.PHP · cURL
<?php
function captchaValid(string $token): bool {
$secret = getenv('MAILI_CAPTCHA_SECRET');
if (!$secret || !$token) return false;
$curl = curl_init('https://xn--80aatf4czb.xn--80aqcic.xn--p1ai/v1/siteverify');
curl_setopt_array($curl, [
CURLOPT_POST => true,
CURLOPT_POSTFIELDS => http_build_query([
'secret' => $secret, 'response' => $token,
'hostname' => 'example.ru', 'action' => 'signup',
]),
CURLOPT_RETURNTRANSFER => true,
CURLOPT_CONNECTTIMEOUT => 3,
CURLOPT_TIMEOUT => 5,
CURLOPT_SSL_VERIFYPEER => true,
CURLOPT_SSL_VERIFYHOST => 2,
]);
$body = curl_exec($curl);
$status = curl_getinfo($curl, CURLINFO_HTTP_CODE);
curl_close($curl);
if ($body === false || $status !== 200) return false;
$result = json_decode($body, true);
return is_array($result)
&& ($result['success'] ?? false) === true
&& ($result['hostname'] ?? '') === 'example.ru'
&& ($result['action'] ?? '') === 'signup';
}
$token = (string) ($_POST['maili-captcha-response'] ?? '');
if (!captchaValid($token)) {
http_response_code(403);
exit('Повторите проверку защиты.');
}
// Здесь — проверка данных и выполнение действия.
?>Токен действует 5 минут и принимается только один раз. При таймауте, любом неуспешном ответе, ошибке вашего приложения после проверки или повторной отправке формы нужен новый токен. API не поддерживает повторное принятие по idempotency key.
JavaScript API
Для SPA можно создать виджет вручную. Контейнер без класса maili-captcha не будет обработан автоматически. Вызовите render после события load скрипта.
<div id="captcha-container"></div>
// После загрузки /v1/widget.js:
const id = mailiCaptcha.render('#captcha-container', {
sitekey: 'ВАШ_ПУБЛИЧНЫЙ_КЛЮЧ',
action: 'contact',
callback: token => { /* разрешить отправку формы */ },
expiredCallback: () => { /* отключить отправку */ },
errorCallback: code => { /* показать сообщение */ }
});
mailiCaptcha.getResponse(id); // токен или пустая строка
mailiCaptcha.reset(id); // новая проверка после отправки
mailiCaptcha.remove(id); // убрать виджет при смене экранаSDK добавляет поле в контейнер; для обычного POST разместите контейнер внутри <form>. В AJAX передавайте getResponse(id) в ваше API. При нескольких виджетах на одной странице передавайте их ID явно; для одной формы можно задать отдельное имя поля через responseFieldName.
Доступны DOM-события на контейнере: maili-captcha-success (токен в event.detail), maili-captcha-expired, maili-captcha-error (код ошибки). Для HTML есть data-callback, data-expired-callback, data-error-callback с именами глобальных функций.
Ответы и ошибки
{
"success": true,
"hostname": "example.ru",
"action": "signup",
"challenge_ts": "2026-10-06T18:35:00+00:00",
"error-codes": []
}hostname возвращается в ASCII/IDNA. Время — ISO 8601. Неуспешная проверка:
{"success": false, "error-codes": ["timeout-or-duplicate"]}| Код | Что делать |
|---|---|
invalid-secret | Проверьте секрет на сервере и статус сайта в кабинете. |
invalid-response | Токен отсутствует, повреждён или выдан для другого подключения. Обновите виджет. |
timeout-or-duplicate | Токен истёк, отозван или уже принят. Нужна новая проверка. |
context-mismatch | Не совпадает ожидаемый домен или действие. |
origin-not-allowed | Добавьте точный домен в кабинет. Маски не поддерживаются. |
invalid-origin / invalid-action | Проверьте обязательные параметры hostname и action. |
ip-mismatch | Проверьте доверенную прокси-цепочку или не передавайте необязательный remoteip. |
rate-limited | HTTP 429. Остановите частые запросы и повторите позже. |
invalid-content-type / request-too-large | HTTP 415 / 413. Используйте поддерживаемый Content-Type, тело до 4 КБ. |
Неуспешные проверки обычно возвращают HTTP 200 с success: false. При 429 или недоступности сервера может прийти ответ от Nginx вместо JSON; обрабатывайте это как неуспешную проверку.
Защита и ограничения
Виджет выполняет SHA-256 proof-of-work в отдельном потоке браузера. Сервер проверяет решение, контекст и срок действия. Лимиты общие для всех процессов приложения и хранятся в базе данных.
- Задача: 10 минут; успешный токен: 5 минут; до 5 попыток решения.
- До 30 задач на IP / IPv6-подсеть за 10 минут; до 500 на сайт; до 1000 на весь сервис.
- API проверки: до 600 запросов в минуту на подключение и IP сервера сайта; до 3000 на весь сервис. Лимиты используют фиксированные временные окна. Nginx дополнительно ограничивает запросы с одного IP до 8 в секунду с запасом на короткие серии.
- До 10 подключений на аккаунт, до 10 точных HTTPS-доменов на подключение.
- Cookies и аккаунт Майли не нужны посетителям внешних сайтов. В базе проверок IP хранится в виде HMAC; записи задач и лимитов старше 24 часов очищаются раз в час. Итоговые счётчики сайта сохраняются.
Это вычислительный барьер и ограничение автоматических запросов, а не доказательство того, что посетитель — человек. Используйте собственные лимиты, CSRF, проверку данных и защиту от подбора паролей. Не заявляйте, что одна галочка исключает ботов или защищает сервер от DDoS.
Сервис использует ресурсы вашего VPS вместе с Майли. Для массового публичного использования стоит выделить отдельные процессы приложения, базу и инфраструктуру; текущие лимиты рассчитаны на начальный запуск.
CSP и диагностика
Если у вашего сайта настроена Content Security Policy, добавьте домен сервиса к существующим script-src и frame-src. Не заменяйте целиком вашу политику этим фрагментом:
script-src 'self' https://xn--80aatf4czb.xn--80aqcic.xn--p1ai;
frame-src https://xn--80aatf4czb.xn--80aqcic.xn--p1ai;JavaScript API проверки вызывается только вашим сервером; прямой запрос из браузера с секретом не поддерживается. Сервис не разрешает CORS для siteverify.
- Пустой виджет: проверьте HTTPS, ключ, точный домен и блокировки CSP / расширения браузера.
- «Этот домен не подключён»:
www.example.ruдобавляется отдельно отexample.ru. - Токен отклонён: проверьте действие, домен, текущий секрет и повторное использование токена.
- HTTP 429: не запускайте проверку в цикле; остановите отправку и повторите позже.