Привет, Хабр! На связи разработчик Peakline — аналитической платформы для Strava. Сегодня я хочу поделиться опытом внедрения Cloudflare Turnstile в веб-приложение на FastAPI. Это решение позволило мне отказаться от назойливых CAPTCHA, улучшить пользовательский опыт и при этом надежно защитить формы регистрации и входа от ботов.
Боль традиционных CAPTCHA
Каждый, кто хоть раз вводил логин и пароль в интернете, сталкивался с ними: "Выберите все светофоры", "Введите искаженный текст", "Я не робот". Классические CAPTCHA, особенно reCAPTCHA от Google, стали синонимом раздражения. Они не только прерывают пользовательский путь, но и вызывают вопросы о конфиденциальности, собирая огромное количество данных.
Для моего проекта, где важен быстрый и удобный доступ к аналитике, такой барьер был неприемлем. Я искал решение, которое было бы:
- Эффективным против ботов. 
- Незаметным для легитимных пользователей. 
- Простым в интеграции. 
- Бесплатным. 
И я его нашел.

Что такое Cloudflare Turnstile?
Turnstile — это современная альтернатива CAPTCHA от Cloudflare. Его главная фишка — он не заставляет пользователей решать головоломки. Вместо этого Turnstile запускает в браузере небольшой набор неинтрузивных JavaScript-тестов, которые собирают данные о поведении и окружении браузера. Эти сигналы отправляются на сервер Cloudflare, где умные алгоритмы определяют, человек перед ними или бот.
Ключевые преимущества:
- Невидимость для пользователя: В большинстве случаев проверка проходит в фоновом режиме за доли секунды. Никаких кликов и картинок. 
- Конфиденциальность: Turnstile не использует cookie и не отслеживает пользователей между сайтами для построения рекламных профилей, в отличие от reCAPTCHA. 
- Простота интеграции: Добавить Turnstile на сайт можно буквально за 5 минут. 
- Бесплатность: Сервис полностью бесплатен для неограниченного числа проверок. 
Шаг 1: Интеграция на фронтенде
Для начала нужно получить ключи (Site Key и Secret Key) в панели управления Cloudflare. Это бесплатно, даже если ваш сайт не использует другие сервисы Cloudflare.

Теперь добавим виджет на страницы регистрации (register.html) и входа (email_login.html). Я использую шаблонизатор Jinja2, но логика будет аналогична для любого фреймворка.
1. Подключаем скрипт. В секцию <head> добавляем скрипт Turnstile.
<script src="https://challenges.cloudflare.com/turnstile/v0/api.js" async defer></script>2. Добавляем виджет в форму. Внутри тега <form> размещаем div-элемент, который и будет нашим виджетом.
<form id="login-form">
    <!-- Поля для email и пароля -->
    <div class="form-group">
        <label for="email" class="form-label">Email</label>
        <input type="email" id="email" name="email" class="form-input" required>
    </div>
    <div class="form-group">
        <label for="password" class="form-label">Пароль</label>
        <input type="password" id="password" name="password" class="form-input" required>
    </div>
    <!-- Вот и сама капча! -->
    <div class="cf-turnstile"
         data-sitekey="{{ TURNSTILE_SITE_KEY }}"
         data-theme="auto">
    </div>
    <button type="submit" class="btn btn-primary">Войти</button>
</form>Обратите внимание на атрибут data-sitekey. Я передаю его из бэкенда как переменную окружения, чтобы не "зашивать" ключ прямо в HTML. data-theme="auto" автоматически подстроит внешний вид виджета под светлую или темную тему вашего сайта.
Когда пользователь отправляет форму, Turnstile автоматически добавляет в данные формы скрытое поле cf-turnstile-response. Мне нужно только извлечь его с помощью JavaScript и отправить на бэкенд.
document.getElementById('login-form').addEventListener('submit', async (e) => {
    e.preventDefault();
    const formData = new FormData(e.target);
    const data = {
        email: formData.get('email'),
        password: formData.get('password'),
        // Получаем токен от виджета
        "cf-turnstile-response": formData.get('cf-turnstile-response')
    };
    // Отправляем данные на наш API
    await fetch('/api/auth/login', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify(data)
    });
});Фронтенд готов!

Шаг 2: Валидация на бэкенде (FastAPI)
Самое интересное происходит на сервере. Мне нужно взять полученный токен cf-turnstile-response и отправить его в Cloudflare для проверки.
1. Модели Pydantic. Я использую Pydantic для валидации входящих данных. Добавим поле для токена в модели входа и регистрации.
from pydantic import BaseModel, Field
class EmailLogin(BaseModel):
    email: str
    password: str
    cf_turnstile_response: str = Field(..., alias="cf-turnstile-response")
class EmailRegistration(BaseModel):
    email: str
    password: str
    # ... другие поля
    cf_turnstile_response: str = Field(..., alias="cf-turnstile-response")
2. Асинхронная функция проверки. Напишем простую асинхронную функцию, которая будет делать POST-запрос к API Cloudflare. Для этого идеально подходит библиотека aiohttp.
import os
import aiohttp
from loguru import logger # Я использую loguru для логирования
async def verify_turnstile(token: str, ip: str) -> bool:
    """Проверяет токен Cloudflare Turnstile."""
    secret_key = os.getenv("TURNSTILE_SECRET_KEY")
    if not secret_key:
        logger.error("TURNSTILE_SECRET_KEY is not set. Skipping validation.")
        # В проде здесь лучше возвращать False
        return True
    payload = {
        "secret": secret_key,
        "response": token,
        "remoteip": ip
    }
    try:
        async with aiohttp.ClientSession() as session:
            async with session.post("https://challenges.cloudflare.com/turnstile/v0/siteverify", data=payload) as response:
                if response.status == 200:
                    result = await response.json()
                    if result.get("success"):
                        logger.info(f"Turnstile validation successful for IP: {ip}")
                        return True
                    else:
                        logger.warning(f"Turnstile validation failed for IP: {ip}. Errors: {result.get('error-codes')}")
                        return False
                else:
                    logger.error(f"Failed to verify Turnstile token. Status: {response.status}")
                    return False
    except Exception as e:
        logger.exception(f"An exception occurred during Turnstile verification: {e}")
        return False
3. Интеграция в эндпоинты. Теперь вызовем нашу функцию в самом начале эндпоинтов регистрации и входа.
from fastapi import FastAPI, Request, HTTPException
app = FastAPI()
@app.post("/api/auth/login")
async def login_email_user(request: Request, login_data: EmailLogin):
    """Вход пользователя по email и паролю с проверкой Turnstile."""
    # Первым делом — проверка CAPTCHA!
    if not await verify_turnstile(login_data.cf_turnstile_response, request.client.host):
        raise HTTPException(status_code=403, detail="Invalid CAPTCHA. Please try again.")
    # ... остальная логика входа ...
    return {"status": "success"}
@app.post("/api/auth/register")
async def register_email_user(request: Request, registration: EmailRegistration):
    """Регистрация нового пользователя с проверкой Turnstile."""
    
    # И здесь тоже
    if not await verify_turnstile(registration.cf_turnstile_response, request.client.host):
        raise HTTPException(status_code=403, detail="Invalid CAPTCHA. Please try again.")
    # ... остальная логика регистрации ...
    return {"status": "success"}
Готово! Теперь формы надежно защищены.
Заключение
Интеграция Cloudflare Turnstile заняла у меня меньше часа, но принесла огромную пользу. Я избавился от раздражающей капчи, сделал процесс входа и регистрации более плавным и не пожертвовал безопасностью. Если вы ищете современное, удобное и бесплатное решение для защиты от ботов — настоятельно рекомендую попробовать Turnstile.
Надеюсь, мой опыт будет вам полезен. Успешных проектов!
Комментарии (20)
 - 0xC0CAC01A06.07.2025 10:50- И как это помогает от ботов с браузером на Selenium?  - cyberscoper Автор06.07.2025 10:50- Cloudflare хоть как-то отсекает ботов с Selenium, особенно с включённой "Managed Challenge" — там он может палить неестественное поведение, отсутствие WebGL, нестандартный User-Agent и т.д. Но если честно пока не думаю что я на столько интересен ботоводам, но думаю стоит изучить эту тему подробнее.  - 0xC0CAC01A06.07.2025 10:50- Кстати, а появились уже готовые ботоводческие библиотеки, которые эмитируют максимально естественное поведение пользователя, плавно двигают мышку, печатают со случайными задержками и периодическим нажатием бекспейса и т.п.?  - NikkiG06.07.2025 10:50- Ты каждого бота на отдельном компьютере запускать будешь со своей мышкой?)) тогда проще индуса посадить) 
 
 
 
 - CloudlyNosound06.07.2025 10:50- Вспомнилось обсуждение противостояния кф и джаббера. - Половина безобидных сайтов и так посещается через vpn, как раз из-за этого защитника. 
 - Anselm_nn06.07.2025 10:50- эта штука тоже заставляет картинки выбирать, не так много, как гугл, но тоже участие пользователя требуется  - m0tral06.07.2025 10:50- Очень редко я бы сказал, самая лучшая каптча, когда вижу гугловские светофоры, зебры, автобусы хочется автора за яйца подвесить, - А за искаженные тексты хочется просто отпинать, их боты на LLM лучше проходят  - nitro8006.07.2025 10:50- Хуже, когда ты все эти светофоры, велосипеды, автобусы и мотоциклы прощелкал - а тебя не пустило. - Вот где бесит 
 
  - Krokochik06.07.2025 10:50- Что? Никогда в жизни не видел картинок от turnstile. И в документации про них нет. Мне кажется, там никакого теста просто нет 
 
 - Pitcentr006.07.2025 10:50- Есть вариант защиты формы без изменения кода, но тоже через CF, можно создать правило в WAF для URL адреса формы это правило будет проверять пользователя на спам либо сразу убрать ряд стран из которых большая вероятность спама, и дальше при отправки формы CF проверяет если есть подозрения будет показана страница с запросом каптчи а если подозрений у CF нет то форма отправляется сразу, в итоге на форме и странице нет никакой отображаемой каптчи, форма чистая, если пользователь честно отправляет форму он даже не попадает на проверку а весь спам попадает на страницу с каптчей на которую они не рассчитывают и пройти не могут. 
 - alexhott06.07.2025 10:50- Лет 15 назад делал форму обратной связи и обсуждали какую капчу использовать, сайт на PHP. - 1 нашли пару библиотек, прикрутили - простенько, но работает. - 2 капча от Гугла - все красиво. - Начали обсуждать риски, Гугл - это же внешний сервис, а что если сломается. На что был выдвинут аргумент "думаешь сервера Гугла менее надежны чем твои?" - Сделали на Гугле - да оно не ломалось, было бесплатное, но после 22-го года пришлось переделывать. - С тех пор я придерживаюсь правила - твое критичное приложение должно быть автономным! 
 - santonrr06.07.2025 10:50- мы тоже перешли на турникет от клаудфларе. но нужно брать в расчёт что обработка происходит иначе ! - он медленнее, и не поддерживает сессию. что значит , будет выполнять проверку перед каждым движением апи. 
 - Dupych06.07.2025 10:50- Капча это не только защита от ботов. - Первые капчи помогали Гуглу распознавать плохоотсканированный текст при оцифровке книг. - Сейчас это обучение ИИ. - Я иногда нажимал не те картинки. Главное количесво 3 или 4. - Проверь и убедишся. - Выбери 2 правильных и одну непоавильную. И тебя может пропустить. 
 
           
 


EnChikiben
С последней тенденцией (
блокировкой/замедление Cloudflare) от РНК как будите бороться?cyberscoper Автор
Но раз уж нужен CF, думаю прятать за другим CDN или ставить что-то вроде fallback на тот случай, если Cloudflare снова "пропадёт".
Вообще, если не под атакой — проще выкинуть его нафиг. Сейчас от него больше проблем, чем пользы, особенно в РФ.
ky0
Второй абзац про РКН или про что, я не совсем понял :)
cyberscoper Автор
Ну да) или же заменить CF на другое, что не заблокировано. Но у моего проекта не только РФ, является рынком