Как создать свою собственную капчу любого типа: Самое подробное руководство
Если вы часто пользуетесь интернетом, то наверняка не раз доказывали сайтам, что вы – человек. И теперь вы задумываетесь: а как создать капчу самостоятельно? Готовых решений существует много, но сделать свою – не только полезно, но и интересно! Мы подготовили для вас расширенный материал с подробной инструкцией и понятными шагами по созданию вашей собственной защиты от кибератак. Предлагаем вам с ним ознакомиться и получить интересный опыт в разработке крутой капча-системы, удобной и защищённой!
Содержание статьи:
Если вы решаете, какую капчу использовать – свою или готовую (например, reCAPTCHA от Google), нужно учитывать удобство, уровень защиты и сложность реализации. Самый главный плюс в том, что уникальные реализации сильно затрудняют автоматическое распознавание. Да, технологии могут быть примерно одинаковы, но всегда можно добавить что-то своё. В нашем руководстве мы будем описывать создание нескольких типов капч. Вот краткая информация о них:
Текстовая капча (ввод текста с картинки)
+ Простая, подходит для небольших форм.
- Легко взламывается, может раздражать пользователей.
Капча с изображениями (сетка, выбор нужных картинок, как у reCAPTCHA)
+ Отличная защита, используется в финансовых учреждениях и голосованиях.
- Неудобна на мобильных устройствах, может быть сложной для некоторых людей.
Капча-слайдер
+ Быстрая и удобная для входа, особенно на мобильных устройствах.
- Может быть легко взломана ботами.
Капча-пазл (передвижение кусочка пазла)
+ Интерактивная, хорошо подходит для игровых и креативных сайтов.
- Требует больше времени на прохождение.
Также можно создать аудио-капчу и проверку на основе поведения пользователя. Для создания аудио-капчи можно использовать Web Audio API, чтобы генерировать и обрабатывать звуки, а для воспроизведения – HTML5 Audio. Если нужно генерировать аудио на сервере, подойдут инструменты Node.js с библиотеками вроде fluent-ffmpeg. Можно также использовать Python с pydub или ffmpeg, а для создания звуков в реальном времени – подойдут и Java, и C#.
Для капчи на основе поведения пользователя удобен JavaScript для отслеживания кликов, движений мыши или прокрутки страницы. Для передачи данных на сервер можно использовать WebSocket. Также FingerprintJS помогает создавать уникальные отпечатки устройства. На сервере это можно реализовать с помощью Python (Flask или Django) или C# (ASP.NET), чтобы собирать данные и обрабатывать их.
Когда лучше использовать готовые системы (reCAPTCHA, Cloudflare, Amazon CAPTCHA и др.)?
- Когда нужна высокая защита с использованием более сложных механизмов
- Если нет желания и ресурсов поддерживать свою капчу или когда не хватает технических навыков для разработки и обновления
- Когда нужна масштабируемость (например, в сервисах с миллионами пользователей)
- Для удобства мобильных пользователей и интеграции с существующими системами
Итак, для простых задач и получения/накопления опыта можно сделать свою капчу (особенно если вы собираетесь потом поддерживать и расширять её), а если защита критична и требует более сложного механизма, лучше выбрать «старые добрые», уже проверенные решения.
Если вы решили сделать капчу самостоятельно, тогда нужно подумать над самой логикой работы. Здесь понадобится и фронтенд – клиентская часть, отображение капчи в браузере, и бэкенд – серверная часть, где будет генерироваться капча и осуществляться проверка ответа пользователя. Давайте рассмотрим примерный этап предстоящей работы:
- Вёрстка формы капчи – создадим интерфейс с изображениями, ползунком или текстовым вводом.
- Генерация данных капчи на сервере – сформируем случайные символы, изображения или элементы пазла.
- Отображение капчи на клиенте – загрузим данные и подстроимся под пользователя.
- Взаимодействие пользователя – отследим клики, перемещения и ввод.
- Проверка ответа пользователя на сервере – сравним введенные данные с правильным значением.
Выбор языка зависит от типа капчи, платформы и уровня защиты.

Для клиентской части (фронтенд) реализация обычно происходит на HTML/CSS/JavaScript. Можно использовать “чистый” подход, а можно обратиться к фреймворкам и библиотекам – React.js (или Vue/Svelte/Angular), Canvas/WebGL, а также можно использовать такие библиотеки:
dragula.js / interact.js – drag-and-drop (это когда нужно нажать мышкой на элемент, перетащить его и отпустить в нужное место)
anime.js / GSAP – анимации
fabric.js / p5.js – рисование на canvas
CryptoJS – если необходимо шифровать что-то на клиенте (например, капча-данные)

Для серверной части (бэкенд) можно выбрать вообще любой язык. Главное, чтобы он:
- Умел принимать запросы от клиента (обычно HTTP)
- Мог обрабатывать логику валидации капчи
- Возвращал ответ (успешная/неуспешная проверка)
Примерно так это выглядит на сервере, независимо от языка:
- Клиент (браузер) отправляет результат прохождения капчи (например, координаты слайдера, токен, текст и т.п.)
- Сервер проверяет: валидный ли ответ? Не истекло ли время? Допустим ли IP?
- Сервер отвечает: "Да, человек" или "Нет, бот"
Выбирайте язык программирования в зависимости от вашего удобства и желаемых технологий:

В наших примерах мы будем использовать связку HTML/CSS/JavaScript + Node.js. Наши капча-системы будут без фреймворков, но максимально защищённые и удобные. Почему мы решили выбрать такой стек:
Node.js отлично подходит для API и логики
Генерация токенов, проверка ответов капчи, хранение состояний сессий и т.д.
Асинхронная модель – удобно обрабатывать множество запросов.
JS в браузере – идеален для визуальных/интерактивных капч
Слайдеры, пазлы, клики по изображениям – всё легко делается на фронте.
Можно отлавливать подозрительное поведение: движения мыши, тайминги, focus/blur и прочее.
Простая интеграция
Капчи на таком стеке легко подключить к любым web-формам, а также удобно добавлять сессии, лимиты, защиту от ботов и т.д.
Теперь, когда мы получили фундамент теоретических знаний и определились с выбором инструментов, можем приступить наконец к практике!
Мы хотим сделать базовую версию капчи, которую в будущем можно будет усложнять и добавлять больше функционала.
Создайте папку с любым названием (например, Textcaptcha), откройте её в своём редакторе кода и создайте файл index.html. Здесь пропишите стили для будущей капчи (вы можете записать их в отдельный файл styles.css, а в index.html оставить разметку, которую опишем ниже), например, такие:
<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Текстовая Капча</title>
<style>
/* Стили для всего тела страницы */
body {
font-family: Arial, sans-serif;
display: flex;
justify-content: center;
align-items: center;
height: 100vh;
background: #f4f4f4;
margin: 0;
}
/* Стили для контейнера капчи */
.captcha-container {
background: white;
padding: 20px;
box-shadow: 0 0 10px rgba(0, 0, 0, 0.1);
text-align: center;
width: 280px;
border-radius: 10px;
}
/* Стили для изображения капчи */
.captcha-image {
display: flex;
justify-content: center;
align-items: center;
margin-bottom: 15px;
border: 2px solid #ddd;
padding: 10px;
background: #fff;
border-radius: 5px;
}
/* Стили для поля ввода капчи */
.captcha-input {
padding: 10px;
width: 100%;
font-size: 14px;
text-align: center;
border: 2px solid #ddd;
border-radius: 5px;
margin-bottom: 10px;
box-sizing: border-box;
}
/* Стили для кнопок (обновить и отправить) */
.buttons {
display: flex;
justify-content: space-between;
align-items: center;
}
/* Стили для кнопки обновления капчи */
.refresh-button {
background: #007bff;
border: none;
padding: 5px;
border-radius: 50%;
cursor: pointer;
display: flex;
justify-content: center;
align-items: center;
}
/* Размер иконки внутри кнопки */
.refresh-button img {
width: 20px;
height: 20px;
}
/* Стили для кнопки отправки */
.submit-button {
background: #28a745;
border: none;
color: white;
padding: 10px 20px;
border-radius: 5px;
cursor: pointer;
}
/* Стили для кнопки, когда она не активна */
.submit-button:disabled {
background: #ccc;
cursor: not-allowed;
}
/* Стили для кнопки, когда она активна и на нее наводится */
.submit-button:hover:not(:disabled) {
background: #218838;
}
/* Стили для сообщения о успешной проверке капчи */
.success-message {
color: green;
display: none;
font-weight: bold;
margin-top: 10px;
}
/* Стили для сообщения о ошибке при проверке капчи */
.error-message {
color: red;
display: none;
font-weight: bold;
margin-top: 10px;
}
</style>
</head>
Также добавим основную разметку для страницы:
<body>
<form class="captcha-container" onsubmit="return false;">
<div class="captcha-image">
<img id="captchaImage" src="" alt="Капча" width="200" height="80">
</div>
<input type="text" id="captchaInput" class="captcha-input" placeholder="Введите текст с картинки" required />
<div class="buttons">
<button type="button" class="refresh-button" id="refreshButton">
<img src="https://cdn-icons-png.flaticon.com/512/61/61444.png" alt="Обновить">
</button>
<button type="submit" class="submit-button" id="submitButton" disabled>Проверить</button>
</div>
<p class="success-message" id="successMessage">Капча пройдена!</p>
<p class="error-message" id="errorMessage">Неверный ввод, попробуйте снова.</p>
</form>
<script src="script.js"></script>
</body>
</html>
Отображение капчи: при загрузке страницы в элемент <img id="captchaImage"> будет загружено изображение капчи через JavaScript.
Обновление капчи: кнопка "Обновить" вызывает функцию, которая обновляет изображение капчи (это будет работать через JavaScript).
Проверка капчи: после ввода текста в поле и нажатия на кнопку "Проверить", введенный текст сравнивается с текстом на изображении капчи. Если ввод правильный — показывается сообщение о успехе, если нет — сообщение об ошибке.
Ожидание ввода: кнопка "Проверить" активируется только после того, как пользователь введет текст в поле.
Вся логика работы капчи реализуется в подключаемом JavaScript-файле (script.js):
- Создайте ещё один файл с названием script.js – здесь будет происходить загрузка капчи и её отправка на сервер (пока что локальный) для проверки. При загрузке страницы будет автоматически загружаться капча.
Пользователь вводит текст с капчи.
При нажатии кнопки "Проверить" отправляется запрос на сервер для проверки текста.
В случае успеха показывается сообщение о правильности ввода, а кнопка становится неактивной.
В случае ошибки показывается сообщение об ошибке, и загружается новая капча.
Пользователь может обновить капчу вручную, нажав на кнопку "Обновить".
Объявим переменную captchaId, она будет хранить уникальный идентификатор капчи. Этот идентификатор используется для проверки введенного текста с конкретной капчей на сервере:
let captchaId = "";
- Создадим функцию fetchCaptcha():
function fetchCaptcha() {
fetch("http://localhost:3000/generate-captcha")
.then((response) => response.json())
.then((data) => {
captchaId = data.captchaId;
document.getElementById("captchaImage").src = data.captchaImage;
})
.catch((error) => console.error("Ошибка загрузки капчи:", error));
}
- Здесь мы отправляем GET-запрос на сервер по адресу http://localhost:3000/generate-captcha для получения нового изображения капчи.
- .then((response) => response.json()): преобразуем ответ от сервера в формат JSON.
- .then((data) => {...}): обработаем полученные данные:
captchaId = data.captchaId: присваивает переменной captchaId значение из ответа сервера. Этот идентификатор используется для проверки правильности введенного текста.
document.getElementById("captchaImage").src = data.captchaImage: изменяет источник изображения с капчей на тот, который пришел с сервера (например, URL изображения).
catch((error) => console.error("Ошибка загрузки капчи:", error)): если произошла ошибка при запросе или обработке ответа, она будет выведена в консоль.
- Добавим обработчик события для кнопки отправки:
document.getElementById("submitButton").addEventListener("click", () => {
const userInput = document.getElementById("captchaInput").value.trim();
if (!userInput) return;
fetch("http://localhost:3000/verify-captcha", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ userInput, captchaId }),
})
.then((response) => response.json())
.then((result) => {
if (result.success) {
document.getElementById("successMessage").style.display = "block";
document.getElementById("errorMessage").style.display = "none";
document.getElementById("submitButton").disabled = true;
} else {
document.getElementById("successMessage").style.display = "none";
document.getElementById("errorMessage").style.display = "block";
fetchCaptcha();
}
})
.catch((error) => console.error("Ошибка при проверке капчи:", error));
});
document.getElementById("submitButton").addEventListener("click", () => {...}): добавляет обработчик события для кнопки "Проверить", которая отправляет введенные данные на сервер для проверки.
const userInput = document.getElementById("captchaInput").value.trim();: получает введенное пользователем значение из поля ввода и удаляет лишние пробелы.
if (!userInput) return;: если поле ввода пустое, обработчик прекращает выполнение.
fetch("http://localhost:3000/verify-captcha", {...}): отправляет POST-запрос на сервер для проверки введенного текста с капчи.
В запросе передаются:
userInput: введённый пользователем текст.
captchaId: идентификатор капчи, полученный ранее.
.then((response) => response.json()): обрабатывает ответ сервера (проверка капчи).
.then((result) => {...}): если капча была успешно проверена (result.success):
Показывается сообщение о успешной проверке капчи.
Кнопка "Проверить" становится неактивной (submitButton.disabled = true).
В случае ошибки:
Показывается сообщение об ошибке.
Вызывается функция fetchCaptcha() для загрузки нового изображения капчи.
- Теперь добавим обработчик события для кнопки обновления капчи:
document
.getElementById("refreshButton")
.addEventListener("click", fetchCaptcha);
При нажатии на эту кнопку будет вызвана функция fetchCaptcha(), которая загрузит новое изображение капчи.
- Обработчик для поля ввода:
document.getElementById("captchaInput").addEventListener("input", (e) => {
document.getElementById("submitButton").disabled =
e.target.value.trim().length === 0;
});
fetchCaptcha();
При каждом изменении текста в поле (событие input) проверяется длина введенного текста.
Если текст пустой, кнопка "Проверить" становится неактивной (submitButton.disabled = true).
Если текст не пустой, кнопка становится активной (submitButton.disabled = false).
- Отлично, стили и код для вызова и проверки капчи реализованы! Теперь переходим к серверной части, где и будет генерироваться капча и проверяться ответ пользователя. Создайте файл server.js, откройте терминал и установите такие зависимости:
npm install express cors uuid canvas
Что мы будем использовать?
express – это фреймворк для Node.js, который упрощает создание веб-серверов.
cors – middleware для разрешения кросс-доменных запросов (для работы с фронтендом).
uuid – для создания уникальных идентификаторов (для генерации captchaId).
canvas – библиотека для работы с графикой (для создания капчи).
- Импортируем установленные зависимости, создадим экземпляр сервера и подключим middleware:
import express from "express";
import cors from "cors";
import { v4 as uuidv4 } from "uuid";
import { createCanvas } from "canvas";
const app = express();
app.use(cors());
app.use(express.json());
- Создадим хранилище для капчи. Объявим объект captchaStore для хранения данных капчи, таких как идентификаторы и текст:
const captchaStore = {};
- Сгенерируем текст капчи. Функция generateCaptchaText генерирует случайную строку, состоящую из 6 символов (буквы и цифры):
function generateCaptchaText() {
return Math.random().toString(36).substring(2, 8).toUpperCase();
}
- Как насчёт того, чтобы добавить случайный цвет? 😀 Функция getRandomColor создаёт случайный цвет в формате RGB:
function getRandomColor() {
const r = Math.floor(Math.random() * 256);
const g = Math.floor(Math.random() * 256);
const b = Math.floor(Math.random() * 256);
return `rgb(${r},${g},${b})`;
}
- Также сгенерируем шрифт:
function getRandomFont() {
const fonts = ["Arial", "Verdana", "Courier", "Georgia", "Times New Roman"];
const randomFont = fonts[Math.floor(Math.random() * fonts.length)];
const randomSize = Math.floor(Math.random() * 10) + 30; // Размер от 30 до 40
return `${randomSize}px ${randomFont}`;
}
- Добавим изображение к тексту, шум и искажения:
function generateCaptchaImage(text) {
const canvas = createCanvas(200, 80);
const ctx = canvas.getContext("2d");
// Заливка фона
ctx.fillStyle = "#f8f8f8";
ctx.fillRect(0, 0, canvas.width, canvas.height);
// Добавляем искажения (шумы, линии)
for (let i = 0; i < 5; i++) {
ctx.strokeStyle = `rgba(0, 0, 0, ${Math.random()})`;
ctx.beginPath();
ctx.moveTo(Math.random() * 200, Math.random() * 80);
ctx.lineTo(Math.random() * 200, Math.random() * 80);
ctx.stroke();
}
// Настройка шрифта и цвета текста
ctx.font = getRandomFont();
ctx.fillStyle = getRandomColor();
ctx.textAlign = "center";
ctx.textBaseline = "middle";
ctx.translate(canvas.width / 2, canvas.height / 2);
ctx.rotate(Math.random() * 0.3 - 0.15); // Небольшой наклон текста
ctx.fillText(text, 0, 0);
return canvas.toDataURL(); // Возвращаем капчу в формате base64
}
Переходим к API:
- API для генерации капчи. В этом обработчике API мы генерируем капчу, сохраняем текст и ID в хранилище и отправляем данные пользователю:
app.get("/generate-captcha", (req, res) => {
const captchaText = generateCaptchaText();
const captchaId = uuidv4();
captchaStore[captchaId] = captchaText;
const captchaImage = generateCaptchaImage(captchaText);
res.json({ captchaId, captchaImage });
});
- API для проверки капчи. В этом обработчике API мы принимаем ввод пользователя и сравниваем его с сохранённым текстом капчи:
app.post("/verify-captcha", (req, res) => {
const { userInput, captchaId } = req.body;
if (!captchaId || !captchaStore[captchaId]) {
return res
.status(400)
.json({ success: false, message: "Капча истекла или не найдена" });
}
const isCorrect = userInput.toUpperCase() === captchaStore[captchaId];
delete captchaStore[captchaId]; // Удаляем капчу после проверки
res.json({
success: isCorrect,
message: isCorrect ? "Капча верна" : "Капча неверна",
});
});
Здесь мы:
- Получаем userInput (введённый текст) и captchaId от пользователя.
- Проверяем, существует ли капча с таким ID в хранилище.
- Если капча есть, сравниваем ввод с сохранённым текстом и отправляем результат (верно или неверно).
- И наконец – запускаем наш сервер на порту 3000!
app.listen(3000, () => console.log("Сервер запущен на порту 3000"));
В терминале запустим сервер командой: node server.js и откроем в браузере наш проект. Ура, капча создана и отлично функционирует!

Существует также такой вид капчи, как слайдер – когда нужно передвинуть ползунок в определённое место в форме – например, вправо. Давайте же скорее создадим его сами!
- Создадим новую папку SimpleSlider, и в редакторе кода создадим файл index.html , куда накидаем основную структуру страницы, включая контейнер для капчи, слайдер и кнопки, добавим стилизацию слайдера, кнопок, сообщений об ошибке и успехе, встроим CSS для оформления страницы и расположения элементов по центру:
<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Простой Слайдер</title>
<style>
body {
font-family: Arial, sans-serif;
display: flex;
justify-content: center;
align-items: center;
height: 100vh;
background: #f4f4f4;
margin: 0;
}
.captcha-container {
background: white;
padding: 20px;
box-shadow: 0 0 10px rgba(0, 0, 0, 0.1);
text-align: center;
width: 280px;
border-radius: 10px;
}
.slider {
width: 100%;
height: 40px;
background: #ddd;
position: relative;
border-radius: 5px;
overflow: hidden;
margin-top: 10px;
}
.slider-button {
width: 40px;
height: 40px;
background: #007bff;
position: absolute;
top: 0;
left: 0;
border-radius: 5px;
cursor: pointer;
}
.success-message,
.error-message {
font-weight: bold;
display: none;
margin-top: 10px;
}
.success-message {
color: green;
}
.error-message {
color: red;
}
</style>
</head>
<body>
<form class="captcha-container" onsubmit="return false;">
<p>Перетащите ползунок вправо</p>
<div class="slider" id="slider">
<div class="slider-button" id="sliderButton"></div>
</div>
<p class="success-message" id="successMessage">Капча пройдена!</p>
<p class="error-message" id="errorMessage">Ошибка, попробуйте снова.</p>
</form>
<script src="script.js"></script>
</body>
</html>
- Теперь создадим JavaScript-файл script.js, который будет отвечать за функционал слайдера, перетаскивание и верификацию через сервер. Создадим переменную captchaSession, в которой будет храниться ID текущей сессии капчи, полученной от сервера, чтобы потом использовать этот ID для валидации на сервере:
let captchaSession = "";
- Загрузка сессии капчи с сервера.
function fetchCaptchaSession() {
fetch("http://localhost:3000/generate-slider-captcha")
.then((response) => response.json())
.then((data) => {
captchaSession = data.sessionId;
})
.catch((error) => console.error("Ошибка загрузки капчи:", error));
}
Здесь мы:
Делаем fetch-запрос на http://localhost:3000/generate-slider-captcha.
Ожидаем от сервера JSON с полем sessionId.
Сохраняем этот sessionId в переменную captchaSession.
Если происходит ошибка — выводит сообщение в консоль.
Это нужно, чтобы позже передать sessionId на проверку в verify-slider.
- Далее получаем доступ к нужным HTML-элементам:
const slider = document.getElementById("slider");
const button = document.getElementById("sliderButton");
const successMessage = document.getElementById("successMessage");
const errorMessage = document.getElementById("errorMessage");
let isDragging = false;
let sliderCompleted = false;
isDragging – следит, активен ли режим перетаскивания.
sliderCompleted – предотвращает повторные действия, если капча уже пройдена.
- Обработаем нажатие мыши на ползунок:
button.addEventListener("mousedown", () => {
if (sliderCompleted) return;
isDragging = true;
});
Когда нажимается кнопка мыши на ползунке, проверяется, прошёл ли пользователь уже капчу. Если нет – включается режим перетаскивания (isDragging = true).
- Перемещение ползунка:
document.addEventListener("mousemove", (e) => {
if (!isDragging) return;
let rect = slider.getBoundingClientRect();
let offsetX = e.clientX - rect.left;
if (offsetX < 0) offsetX = 0;
if (offsetX > rect.width - button.offsetWidth)
offsetX = rect.width - button.offsetWidth;
button.style.left = offsetX + "px";
});
Когда мышь двигается, проверяем, включён ли режим drag, вычисляем координаты мыши относительно слайдера. Значение ограничивается границами слайдера. Устанавливаем left у кнопки (ползунка), чтобы она двигалась.
- Завершаем действие отпусканием мыши:
document.addEventListener("mouseup", () => {
if (!isDragging) return;
isDragging = false;
let rect = slider.getBoundingClientRect();
let finalPosition = parseInt(button.style.left);
Проверяем, происходило ли перетаскивание, выключаем режим isDragging, получаем конечную позицию ползунка. И проверяем, дошёл ли ползунок до конца:
if (finalPosition >= rect.width - button.offsetWidth - 5) {
- Отправка на сервер (успешное выполнение):
fetch("http://localhost:3000/verify-slider", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ sessionId: captchaSession, completed: true }),
})
Отправляем POST-запрос на /verify-slider. В теле запроса указываем:
- sessionId – ID сессии.
- completed: true – пользователь довёл ползунок до конца.
- Обрабатываем ответ.
Если сервер ответил success: true:
- Показывается сообщение об успехе.
- Кнопка меняет цвет.
- Капча считается завершённой.
Если сервер ответил ошибкой:
- Показывается сообщение об ошибке.
- Ползунок сбрасывается.
- Загружается новая капча.
.then((response) => response.json())
.then((result) => {
if (result.success) {
successMessage.style.display = "block";
errorMessage.style.display = "none";
button.style.background = "green";
sliderCompleted = true;
} else {
successMessage.style.display = "none";
errorMessage.style.display = "block";
button.style.left = "0px";
fetchCaptchaSession();
}
})
- Можем также добавить обработку ошибок запроса: сбрасываем ползунок, запрашиваем новую капчу.
.catch((error) => {
console.error("Ошибка при проверке капчи:", error);
errorMessage.style.display = "block";
button.style.left = "0px";
fetchCaptchaSession();
});
- Ползунок не дошёл до конца. Если пользователь не довёл до конца – ползунок сбрасывается обратно влево:
} else {
button.style.left = "0px";
}
});
fetchCaptchaSession();
- Переходим к серверной части. Создадим в этой же директории файл server.js, откроем терминал и установим нужные нам зависимости:
npm install express cors body-parser
express нужен для работы HTTP-сервера.
cors – разрешает кросс-доменные запросы.
body-parser – для обработки JSON-тела POST-запросов.
- Импортируем их в наш проект:
import express from "express";
import cors from "cors";
import bodyParser from "body-parser";
- Настроим сервер. Создадим экземпляр Express-приложения и укажем порт (например, 3000):
const app = express();
const PORT = 3000;
- Разрешаем запросы с других доменов. Указываем Express обрабатывать JSON в теле запроса. Обозначаем хранилище сессий капчи: используется Map для хранения временных сессий (в будущем лучше использовать Redis). SESSION_TTL – срок жизни сессии: 5 минут.
const sliderSessions = new Map();
const SESSION_TTL = 5 * 60 * 1000;
- Очищаем устаревшие сессии. Каждую минуту запускается проверка всех сессий, если сессия устарела (старше 5 минут) – удаляется:
function cleanUpSessions() {
const now = Date.now();
for (const [sessionId, { timestamp }] of sliderSessions) {
if (now - timestamp > SESSION_TTL) {
sliderSessions.delete(sessionId);
}
}
}
setInterval(cleanUpSessions, 60 * 1000); // Запуск каждую минуту
- Создаём новую капчу. Генерируем sessionId. Он сохраняется в память вместе с текущим временем. Клиенту возвращается sessionId, который будет использоваться при проверке.
app.get("/generate-slider-captcha", (req, res) => {
const sessionId = Math.random().toString(36).substring(2, 15);
sliderSessions.set(sessionId, { completed: false, timestamp: Date.now() });
console.log(`Новая сессия: ${sessionId}`);
res.json({ sessionId });
});
- Проверка капчи и запуск сервера. Получаем sessionId и completed (булево значение) из тела запроса.
Если сессия не найдена – ошибка.
Если пользователь успешно прошёл слайдер – обновляем сессию и возвращаем успех.
app.post("/verify-slider", (req, res) => {
const { sessionId, completed } = req.body;
if (!sessionId || !sliderSessions.has(sessionId)) {
return res
.status(400)
.json({ success: false, message: "Сессия не найдена" });
}
if (completed) {
sliderSessions.set(sessionId, { completed: true, timestamp: Date.now() });
console.log(`Капча пройдена: ${sessionId}`);
return res.json({ success: true });
} else {
return res
.status(400)
.json({ success: false, message: "Капча не пройдена" });
}
});
app.listen(PORT, () => {
console.log(`Сервер запущен на http://localhost:${PORT}`);
});
Запустим наш сервер вводом команды в терминал node server.js и откроем в браузере весь проект. Получилась вот такая простая, но функциональная капча-ползунок!

Если просто хранить всё в памяти – будет быстро, но ненадёжно: сервер упал, и всё пропало. Поэтому лучше использовать Redis для кэширования данных – он быстрый, умеет работать с TTL и идеально подходит для временных данных вроде капчи и токенов.
Вот примерная схема: генерируете капчу – сохраняете в Redis с ID – выдаёте пользователю этот ID через куки или query – он решает – вы сверяете и выдаёте или не выдаёте токен. Токен тоже сохраняете в Redis, чтобы потом проверить при логине или отправке формы.
Куки нужны для того, чтобы помнить: "Этот пользователь уже прошёл капчу". Можно поставить куки на 10 минут или на 1 час – как вам захочется. После этого времени посетителю сайта снова откроется капча.
Можно добавить таймеры, чтобы не спамили, лимиты по IP и другую полезную автоматику. Redis это всё отлично делает.
Redis – супер для временного хранения, но иногда нужно хранить статистику долговременно: кто сколько раз решал, откуда, как часто ошибался, какой тип капчи использовался и т.п. Вот тут уже стоит подключить базу данных – Postgres, Mongo или даже MySQL.
База данных нужна для логов, аналитики и отслеживания подозрительных действий.
Дополнительные параметры:
Кроме captchaId для капч обычно используются такие параметры:

Что с этим делать?
Добавляйте эти поля в объект при генерации капчи и отправляйте в Redis + базу. При проверке капчи обновляйте статус (успешно/неуспешно), инкрементируйте попытки. При успехе можно даже сохранить токен авторизации сразу в Redis, связав с captchaId или IP. Если всё хорошо настроить, вы получите не просто защиту, а целую систему антибот-контроля и при желании – панель статистики, где видно: кто, где, когда и как решал капчи.
Для защиты капчи можно также добавлять:
- Шум, искажения, случайные параметры (как мы делали в текстовой капче).
- Анализ движения мыши (для слайдера/пазла).
- Обфускацию и шифрование
Не передавайте координаты или хеши на клиент. Подписывайте данные (например, JWT).
От теории перейдём к практике! Представим себе проект, где для входа в систему нужно не только ввести логин и пароль, но и пройти капчу в виде пазла. В этой капче пользователь должен перетащить кусочек изображения (вырезать кусочки будем тут же в нашем проекте) в правильное место на фоне. Сервер проверяет токен капчи, и только если капча пройдена и данные логин/пароль прошли проверку на сервере, пользователь может успешно авторизоваться. Перейдём к реализации этой идеи!
1. Для начала подготовим все нужные нам инструменты и файлы. Создадим папку Puzzleslider и откроем её в редакторе кода. Теперь можем добавить картинку фона капчи с размером 320x180 пикселей в папку images. Это будет основа для нашего пазла.
2. Мы хотим хранить и управлять сессиями и токенами с помощью Redis, поэтому установим его на свой компьютер и запустим сервер redis (на официальном сайте можно найти всю необходимую информацию):
Запущенный redis-server на ОС Windows:

3. Теперь в папке Puzzleslider создадим файл index.html и пропишем стили с помощью встроенного CSS и разметку для страницы:
<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Капча + Авторизация</title>
<style>
body {
font-family: Arial, sans-serif;
background: #f4f4f4;
display: flex;
justify-content: center;
align-items: center;
height: 100vh;
}
.container {
background: white;
padding: 20px;
width: 320px;
box-shadow: 0 0 10px rgba(0, 0, 0, 0.1);
}
.captcha-image {
width: 100%;
height: 180px;
position: relative;
margin-bottom: 10px;
}
canvas {
position: absolute;
top: 0;
left: 0;
}
.slider {
width: 100%;
height: 40px;
background: #ddd;
border-radius: 5px;
position: relative;
cursor: pointer;
}
.slider-button {
width: 40px;
height: 40px;
background: #007bff;
position: absolute;
top: 0;
left: 0;
border-radius: 5px;
transition: left 0.2s;
}
.success,
.error {
margin-top: 10px;
font-weight: bold;
}
input {
width: 100%;
padding: 10px;
margin-top: 10px;
box-sizing: border-box;
}
button {
width: 100%;
margin-top: 10px;
padding: 10px;
background: #28a745;
border: none;
color: white;
cursor: pointer;
font-weight: bold;
}
</style>
</head>
<body>
<div class="container">
<h3>Проверка капчи</h3>
<form id="captchaForm">
<div class="captcha-image">
<canvas id="puzzleCanvas"></canvas>
<canvas id="puzzlePieceCanvas"></canvas>
</div>
<div class="slider" id="slider">
<div class="slider-button" id="sliderButton"></div>
</div>
<p class="success" id="captchaStatus"></p>
</form>
<h3>Вход</h3>
<form id="loginForm">
<input type="text" id="username" placeholder="Имя пользователя" />
<input type="password" id="password" placeholder="Пароль" />
<button id="loginButton" type="submit">Войти</button>
<p class="error" id="loginStatus"></p>
</form>
</div>
<script src="script.js"></script>
</body>
</html>
Этот код создаёт страницу с двумя основными блоками: форму капчи и форму авторизации. В первом блоке находится капча, состоящая из двух канвасов для изображения и его части, а также слайдера для проверки решения капчи. Во втором блоке – форма для ввода имени пользователя и пароля, с кнопкой "Войти" для авторизации. Каждый из блоков оформлен с использованием HTML-элементов, и взаимодействие с ними будет обрабатываться в JavaScript через внешний файл script.js.
Здесь весь процесс будет состоять из трёх основных частей:
- Генерация и проверка капчи.
- Движение слайдера и проверка его позиции.
- Отправка данных формы для авторизации после успешной проверки капчи.
4. Реализуем функциональность капчи с пазлом и защищённую форму авторизации. Для этого создадим новый файл script.js и в нём инициализируем переменные:
let sessionId = "";
let cutoutX = 0;
let cutoutY = 0;
let isDragging = false;
const puzzleWidth = 320;
const puzzleHeight = 180;
const pieceSize = 50;
sessionId: будет хранить уникальный идентификатор сессии, который используется для проверки капчи на сервере.
cutoutX, cutoutY: координаты части изображения, которая будет вырезана и преобразована в пазл.
isDragging: флаг для отслеживания состояния перетаскивания слайдера.
puzzleWidth, puzzleHeight, pieceSize: задают размеры капчи (изображения) и размера пазла.
5. Получим элементы из HTML, с которыми будет взаимодействовать JavaScript:
const puzzleCanvas = document.getElementById("puzzleCanvas");
const puzzlePieceCanvas = document.getElementById("puzzlePieceCanvas");
const slider = document.getElementById("slider");
const sliderButton = document.getElementById("sliderButton");
const captchaStatus = document.getElementById("captchaStatus");
const loginButton = document.getElementById("loginButton");
puzzleCanvas и puzzlePieceCanvas – канвасы для отображения изображения и вырезанного кусочка пазла.
slider и sliderButton – элементы слайдера, который пользователь будет перемещать.
captchaStatus – элемент для отображения сообщений о статусе капчи.
loginButton – кнопка для отправки формы авторизации.
6. Создадим функцию для запроса новой капчи:
function fetchCaptcha() {
sessionId = "";
fetch("http://localhost:3000/generate-puzzle-captcha")
.then(res => res.json())
.then(data => {
sessionId = data.sessionId;
cutoutX = data.cutoutX;
cutoutY = data.cutoutY;
const bgImg = new Image();
bgImg.src = `${data.backgroundImage}?${Date.now()}`;
bgImg.onload = () => drawCaptcha(bgImg);
resetState();
})
.catch(err => console.error("Ошибка капчи:", err));
}
fetchCaptcha() делает запрос на локальный сервер (http://localhost:3000/generate-puzzle-captcha), чтобы получить данные для генерации новой капчи.
Сервер возвращает данные: sessionId (идентификатор сессии), cutoutX, cutoutY (координаты для вырезания части изображения) и backgroundImage (ссылка на фоновое изображение).
Загружается изображение, и когда оно загружено, вызывается функция drawCaptcha для отрисовки капчи на канвасе.
Вызов resetState() сбрасывает состояние интерфейса.
7. Рисуем капчу и вырезаем кусочек пазла:
function drawCaptcha(img) {
puzzleCanvas.width = puzzlePieceCanvas.width = puzzleWidth;
puzzleCanvas.height = puzzlePieceCanvas.height = puzzleHeight;
const bgCtx = puzzleCanvas.getContext("2d");
const pieceCtx = puzzlePieceCanvas.getContext("2d");
bgCtx.drawImage(img, 0, 0, puzzleWidth, puzzleHeight);
pieceCtx.clearRect(0, 0, puzzleWidth, puzzleHeight);
pieceCtx.drawImage(
img, cutoutX, cutoutY, pieceSize, pieceSize,
0, 0, pieceSize, pieceSize
);
bgCtx.clearRect(cutoutX, cutoutY, pieceSize, pieceSize);
}
- На puzzleCanvas рисуется полное изображение.
- На puzzlePieceCanvas рисуется только вырезанный кусок изображения (размером pieceSize).
- Затем на фоне капчи убирается кусок изображения с координатами cutoutX и cutoutY.
8. Сброс состояния капчи – слайдер и кусок изображения возвращаются в начальное положение. Также блокируется кнопка входа до тех пор, пока капча не будет пройдена:
function resetState() {
sliderButton.style.left = "0px";
puzzlePieceCanvas.style.left = "0px";
puzzlePieceCanvas.style.top = `${cutoutY}px`;
captchaStatus.textContent = "";
loginButton.disabled = true;
}
9. Обработаем начало перетаскивания слайдера:
sliderButton.addEventListener("mousedown", () => {
isDragging = true;
const rect = slider.getBoundingClientRect();
const sliderLeft = rect.left;
const move = (e) => {
if (!isDragging) return;
let x = e.clientX - sliderLeft - 20;
x = Math.max(0, Math.min(x, slider.offsetWidth - 40));
sliderButton.style.left = `${x}px`;
puzzlePieceCanvas.style.left = `${x}px`;
puzzlePieceCanvas.style.top = `${cutoutY}px`;
};
const stop = () => {
isDragging = false;
document.removeEventListener("mousemove", move);
document.removeEventListener("mouseup", stop);
const userX = parseInt(puzzlePieceCanvas.style.left, 10);
fetch("http://localhost:3000/verify-puzzle", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ sessionId, position: userX }),
})
.then(res => res.json())
.then(data => {
if (data.success) {
localStorage.setItem("captchaToken", data.token);
captchaStatus.textContent = "Капча пройдена ✅";
captchaStatus.style.color = "green";
loginButton.disabled = false;
} else {
captchaStatus.textContent = "Неверно. Попробуйте снова.";
captchaStatus.style.color = "red";
setTimeout(fetchCaptcha, 1000);
}
});
};
document.addEventListener("mousemove", move);
document.addEventListener("mouseup", stop);
});
- Когда пользователь нажимает на кнопку слайдера (mousedown), начинается процесс перетаскивания.
- Отслеживается движение мыши (mousemove), и слайдер и кусок пазла двигаются синхронно.
- Когда пользователь отпускает кнопку мыши (mouseup), проверяется, насколько сдвинут слайдер, и отправляется запрос на сервер для проверки правильности решения капчи.
10. Вход:
document.getElementById("loginButton").addEventListener("click", (event) => {
event.preventDefault();
const username = document.getElementById("username").value;
const password = document.getElementById("password").value;
const captchaToken = localStorage.getItem("captchaToken");
fetch("http://localhost:3000/login", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ username, password, captchaToken }),
})
.then((res) => res.json())
.then((data) => {
const status = document.getElementById("loginStatus");
if (data.success) {
status.textContent = "Успешный вход!";
status.style.color = "green";
} else {
status.textContent = "Ошибка: " + data.message;
status.style.color = "red";
}
})
.catch((err) => {
const status = document.getElementById("loginStatus");
status.textContent = "Ошибка при авторизации.";
status.style.color = "red";
});
});
fetchCaptcha();
});
- Когда пользователь нажимает кнопку входа, отправляется запрос на сервер с данными для логина (имя пользователя, пароль и токен капчи).
- Если вход успешен, выводится сообщение об успешном входе, иначе – сообщение об ошибке.
11. Стили для капчи и вся необходимая клиентская часть готовы – теперь можно переходить к серверу для хранения данных сессий и токенов, генерацией капчи, её верификации и авторизации пользователей с обязательной проверкой капчи через токен. Создадим новый файл server.js, откроем терминал и установим все нужные нам инструменты:
npm install express cors body-parser @redis/client uuid
express: фреймворк для работы с сервером.
cors: модуль для настройки CORS (Cross-Origin Resource Sharing), который позволяет серверу принимать запросы с других доменов.
body-parser: для парсинга JSON в запросах.
@redis/client: библиотека Redis для взаимодействия с базой данных Redis (которую мы ранее установили и запустили redis-server).
uuid: для создания уникальных сессий и токенов.
12. Импортируем зависимости и настроим сервер:
import express from "express";
import cors from "cors";
import bodyParser from "body-parser";
import { createClient } from "@redis/client";
import { v4 as uuidv4 } from "uuid";
const app = express();
const port = 3000;
app.use(cors());
app.use(bodyParser.json());
app.use(express.static("images"));
express() – создаёт экземпляр приложения Express.
port = 3000 – устанавливает порт, на котором будет работать сервер.
app.use(cors()) – включает CORS, разрешая запросы с других доменов.
app.use(bodyParser.json()) – конфигурирует сервер для парсинга JSON в теле запросов.
app.use(express.static("images")) – указывает Express обслуживать статичные файлы из папки "images" (будет использоваться ранее загруженное изображение 320*180).
13. Подключаемся к Redis:
const redisClient = createClient();
redisClient.on("error", (err) => console.log("Redis error:", err));
redisClient.connect();
14. Генерируем и проверяем капчу:
app.get("/generate-puzzle-captcha", async (req, res) => {
const sessionId = uuidv4();
const cutoutX = Math.floor(Math.random() * (320 - 50));
const cutoutY = Math.floor(Math.random() * (180 - 50));
await redisClient.set(sessionId, JSON.stringify({ cutoutX, cutoutY }), { EX: 300 });
res.json({
sessionId,
cutoutX,
cutoutY,
backgroundImage: "images/captcha-background.jpg",
});
});
app.post("/verify-puzzle", async (req, res) => {
const { sessionId, position } = req.body;
const session = await redisClient.get(sessionId);
if (!session) return res.status(400).json({ success: false });
const { cutoutX } = JSON.parse(session);
if (Math.abs(position - cutoutX) <= 10) {
const token = uuidv4();
await redisClient.set(`captcha-token:${token}`, "valid", { EX: 300 });
return res.json({ success: true, token });
} else {
return res.json({ success: false });
}
});
Здесь создаётся маршрут POST /verify-puzzle, который проверяет правильность решения капчи.
sessionId и position (позиция ползунка) передаются в теле запроса.
const session = await redisClient.get(sessionId) – получаем данные сессии из Redis. Если сессия не существует, возвращаем ошибку 400.
const { cutoutX } = JSON.parse(session) – извлекаем координаты выреза из сессии.
Проверяем, насколько сильно позиция отличается от правильной (Math.abs(position - cutoutX) <= 10). Если разница в пределах 10 пикселей – капча пройдена.
Если капча пройдена, генерируется токен и сохраняется в Redis, чтобы его можно было использовать для дальнейших действий (срок действия токена – 300 секунд).
Ответ с токеном отправляется клиенту.
15. Авторизация с проверкой капчи:
app.post("/login", async (req, res) => {
const { username, password, captchaToken } = req.body;
if (!captchaToken) {
return res.status(400).json({ success: false, message: "Нет токена капчи" });
}
// Проверяем валидность токена капчи
const validCaptcha = await redisClient.get(`captcha-token:${captchaToken}`);
if (!validCaptcha) {
return res.status(403).json({ success: false, message: "Капча не пройдена" });
}
// Пример проверки
if (username === "test" && password === "password") {
res.json({ success: true });
} else {
res.status(401).json({ success: false, message: "Неверные учетные данные" });
}
});
app.listen(port, () => console.log(`Сервер на http://localhost:${port}`));
В этой части создаётся POST /login, который обрабатывает запрос на авторизацию.
Проверяется наличие captchaToken в теле запроса. Если токен не передан – возвращается ошибка.
const validCaptcha = await redisClient.get(...) – проверяется, существует ли токен в Redis.
Если токен не найден, значит, капча не пройдена, и возвращается ошибка 403.
Если капча пройдена, происходит проверка логина и пароля. Допустим, у нас логин text и пароль password, это и пропишем в if (username === …)
Если данные совпадают с заранее заданными значениями, возвращается успех.
В случае неверных данных (например, неправильный логин или пароль) возвращается ошибка 401.
Для того, чтобы посмотреть все сессии и ключи, хранящихся в redis, можно использовать команду KEYS *
Например:

16. Проверим, что сервер redis запущен и корректно работает, запустим server.js командой node server.js и откроем проект в браузере. Попробуем потестировать форму авторизации и капчу – если делаем всё правильно – решаем капчу и вводим верные данные, то получаем такой вид:

У нас не получится совершить вход, пока мы не решим капчу и не введём правильные логин-пароль:

Наш код отлично работает! Но можно пойти дальше и создать такое решение, чтобы пользователь не проходил капчу заново каждый раз. Всё это можно реализовать, добавив использование куки в браузер пользователя. Предлагаем вам рассмотреть следующий пример и попробовать разработать полноценную капчу с сохранением токенов и сессий и с добавлением куки:
Давайте напишем код, который будет создавать систему капчи с изображениями, где пользователю нужно будет выбрать картинки, относящиеся к определённой категории – например, машины, животные или природа. Когда пользователь зайдёт на страницу, он получит набор картинок, из которых нужно будет выбрать те, что подходят под категорию. Сервер сгенерирует капчу, выбирая случайные изображения и создавая уникальный код для проверки. После того, как пользователь выберет картинки и нажмёт на кнопку, результат отправится на сервер для проверки того, правильно ли выбраны изображения. Для того, чтобы пользователь не проходил капчу снова – если он уже прошёл её – данные сохраняются в Redis, и используется куки, чтобы отслеживать, прошёл ли он капчу. Да, работа будет немного сложнее, чем в предыдущих примерах, но результат того стоит! Начнём с подготовки:
- Для начала создадим папку с любым удобным вам названием (в нашем примере это Gridcaptcha). Там создадим папку images и добавим несколько картинок размером 150x150 или 100x100 пикселей. Для начала по количеству – не менее 9, но чем больше – тем разнообразнее и сложнее будет капча! Картинки нужно подобрать по категориям. Например, vehicle, animal и nature. И дать соответствующие названия: car.jpg, cat.jpg, mountain.jpg и т.д.
- Установим Redis, как в предыдущем примере.
- Также можно сразу создать файл server.js и в него добавить все нужные нам зависимости:
npm install express cors body-parser morgan @redis/client uuid
express – используется для создания сервера и маршрутов (например, для обработки запросов /get-captcha и /verify-captcha).
cors – это middleware для разрешения кросс-доменных запросов.
body-parser – это middleware для парсинга данных, отправленных в запросах на /verify-captcha.
morgan – это middleware используется с уровнем логирования dev для отображения краткой информации о запросах.
@redis/client — это клиент для подключения к Redis, хранения данных капчи и проверки, был ли пройден запрос пользователя.
uuid – библиотека для генерации уникальных идентификаторов капчи и пользователей, чтобы отслеживать их сессии и связанные данные.
- Создадим index.html и в него добавим структуру и всю внешнюю оболочку нашей будущей капчи:
<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Капча с изображениями</title>
<style>
body {
font-family: Arial, sans-serif;
display: flex;
justify-content: center;
align-items: center;
height: 100vh;
background: #f4f4f4;
margin: 0;
}
.captcha-container {
background: white;
padding: 20px;
box-shadow: 0 0 10px rgba(0, 0, 0, 0.1);
text-align: center;
border-radius: 8px;
}
.captcha-images {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 10px;
margin-bottom: 15px;
}
.captcha-images img {
width: 100px;
height: 100px;
object-fit: cover;
border: 2px solid #ddd;
cursor: pointer;
border-radius: 5px;
transition: transform 0.3s ease;
}
.captcha-images img.selected {
transform: scale(1.1);
border-color: #007bff;
}
.captcha-images img:hover {
transform: scale(1.05);
}
.submit-button {
padding: 10px 20px;
background: #007bff;
color: white;
border: none;
cursor: pointer;
border-radius: 5px;
}
.submit-button:disabled {
background: #ddd;
cursor: not-allowed;
}
.success-message {
color: green;
display: none;
margin-top: 10px;
}
.error-message {
color: red;
display: none;
margin-top: 10px;
}
</style>
</head>
<body>
<div class="captcha-container">
<p id="captchaInstruction">Загрузка капчи...</p>
<div class="captcha-images" id="captchaImages"></div>
<button class="submit-button" id="submitButton" disabled>Проверить</button>
<p class="success-message" id="successMessage">Капча пройдена!</p>
<p class="error-message" id="errorMessage">Неверно выбраны изображения! Попробуйте снова.</p>
</div>
<script src="script.js"></script>
</body>
</html>
Мы отобразим капчу в виде сетки 3x3 с изображениями размером 100x100 пикселей. При выборе изображения оно увеличивается и обводится синим цветом. Кнопка "Проверить" изначально неактивна, но становится доступной после выбора изображений, синие границы и белый текст выделяют её. Сообщения об успешной или неудачной проверке отображаются в зелёном или красном цвете.
- В новом файле script.js создадим несколько переменных и получим капчу с сервера с помощью функции fetchCaptcha:
let selectedImages = new Set();
let captchaId = null;
let userId = null;
document.addEventListener("DOMContentLoaded", async () => {
userId = localStorage.getItem("userId");
if (!userId) {
userId = crypto.randomUUID();
localStorage.setItem("userId", userId);
}
await fetchCaptcha();
});
selectedImages – это коллекция, которая будет хранить выбранные изображения.
captchaId – ID текущей капчи, который будет использоваться для верификации.
userId – идентификатор пользователя, который сохраняется в localStorage – можно также использовать cookies, но в данном случае в этом нет необходимости, так как userId используется только на клиентской стороне и не отправляется с каждым запросом на сервер, например, как при авторизации. Если нет userId, мы генерируем новый ID с помощью crypto.randomUUID() и сохраняем его в localStorage.
- Загрузим капчу с сервера:
async function fetchCaptcha() {
try {
const response = await fetch("http://localhost:3000/get-captcha", {
headers: { "x-user-id": userId },
});
const data = await response.json();
if (data.success) {
document.getElementById("captchaInstruction").textContent =
"Капча уже пройдена!";
document.getElementById("captchaImages").innerHTML = "";
document.getElementById("submitButton").style.display = "none";
return;
}
captchaId = data.captchaId;
document.getElementById(
"captchaInstruction"
).textContent = `Выберите все изображения с ${data.category}`;
renderImages(data.images);
} catch (error) {
console.error("Ошибка загрузки капчи:", error);
alert("Произошла ошибка. Попробуйте снова.");
}
}
- Функция fetchCaptcha делает запрос к серверу, чтобы получить капчу. Запрос содержит заголовок с userId.
- Сервер возвращает JSON-объект, который содержит информацию о том, прошёл ли пользователь капчу (data.success).
- Если капча уже будет пройдена, на странице отображается сообщение, изображения и кнопка скрываются.
- Если капча не будет пройдена, сохраняется captchaId и категория изображений, затем вызывается функция renderImages для изображения выбора.
- Отобразим и обработаем выбор изображений:
function renderImages(images) {
const container = document.getElementById("captchaImages");
container.innerHTML = "";
selectedImages.clear();
document.getElementById("submitButton").disabled = true;
images.forEach((imgData, index) => {
const img = document.createElement("img");
img.src = imgData.src;
img.dataset.index = index;
img.addEventListener("click", () => toggleImageSelection(img, imgData.src));
container.appendChild(img);
});
}
function toggleImageSelection(img, src) {
if (selectedImages.has(src)) {
selectedImages.delete(src);
img.classList.remove("selected");
} else {
selectedImages.add(src);
img.classList.add("selected");
}
document.getElementById("submitButton").disabled = selectedImages.size === 0;
}
- renderImages получает массив изображений и отображает их в контейнере с ID captchaImages.
- Для каждого изображения создаётся элемент <img>, которому присваиваются атрибуты src и data-index. После этого добавляется обработчик события на клик по изображению.
- Когда изображение выбрано, оно будет добавлено в коллекцию selectedImages. Пока ничего не выбрано, кнопка отправки (submitButton) будет заблокирована.
- Когда пользователь кликает по изображению, мы добавляем или удаляем его из коллекции selectedImages.
- Визуально это выражается в изменении класса selected, который масштабирует изображение и меняет его границу на синюю.
- Кнопка отправки будет активирована только в случае, если выбрано хотя бы одно изображение.
- Отправим выбранные изображения на сервер:
document.getElementById("submitButton").addEventListener("click", async () => {
try {
const response = await fetch("http://localhost:3000/verify-captcha", {
method: "POST",
headers: {
"Content-Type": "application/json",
"x-user-id": userId,
},
body: JSON.stringify({
captchaId,
selectedImages: Array.from(selectedImages),
}),
});
const result = await response.json();
document.getElementById("successMessage").style.display = result.success
? "block"
: "none";
document.getElementById("errorMessage").style.display = result.success
? "none"
: "block";
setTimeout(fetchCaptcha, 1500);
} catch (error) {
console.error("Ошибка при отправке капчи:", error);
alert("Ошибка сервера. Попробуйте позже.");
}
});
- Когда пользователь нажимает кнопку "Проверить", мы отправляем POST запрос на сервер по адресу /verify-captcha, передавая captchaId и массив выбранных изображений.
- На основе ответа сервера (JSON с полем success) мы показываем сообщение об успехе или ошибке.
- После проверки капчи, функция setTimeout вызывает fetchCaptcha, чтобы обновить капчу через 1.5 секунды.
- Перейдём к ранее созданному файлу server.js и импортируем уже установленные зависимости:
import express from "express";
import cors from "cors";
import bodyParser from "body-parser";
import morgan from "morgan";
import { createClient } from "@redis/client";
import { v4 as uuidv4 } from "uuid";
- Подключаемся к Redis:
const app = express();
const PORT = 3000;
const redisClient = createClient();
redisClient.connect().catch((err) => console.error("Ошибка Redis:", err));
Здесь:
- Создаётся экземпляр Express приложения (app).
- Устанавливается порт, на котором будет работать сервер (3000).
- Создаётся клиент для Redis и устанавливается соединение
11. Подключаем middleware:
app.use(cors({ origin: true, credentials: true }));
app.use(bodyParser.json());
app.use(morgan("dev"));
cors: разрешает доступ с любых источников и поддерживает передачу куки.
bodyParser: парсит JSON-тела запросов.
morgan: логирует запросы в консоль с помощью формата "dev".
- Определяем, что файлы из папки images будут доступны через URL /images:
app.use("/images", express.static("images"));
- Определим массив изображений: каждый объект содержит путь к изображению и его категорию – в будущем можно будет расширить метаданные и добавлять/извлекать изображения из базы данных).
const images = [
{ src: "images/car1.jpg", category: "vehicle" },
{ src: "images/car2.jpg", category: "vehicle" },
{ src: "images/car3.jpg", category: "vehicle" },
{ src: "images/car4.jpg", category: "vehicle" },
{ src: "images/ship.jpg", category: "vehicle" },
{ src: "images/plain.jpg", category: "vehicle" },
{ src: "images/cat.jpg", category: "animal" },
{ src: "images/dog1.jpg", category: "animal" },
{ src: "images/dog2.jpg", category: "animal" },
{ src: "images/parrot.jpg", category: "animal" },
{ src: "images/tree.jpg", category: "nature" },
{ src: "images/flower.jpg", category: "nature" },
{ src: "images/mountain.jpg", category: "nature" },
{ src: "images/river.jpg", category: "nature" },
];
- Случайным образом выберем заданное количество изображений из массива:
function getRandomItems(arr, count) {
return [...arr].sort(() => 0.5 - Math.random()).slice(0, count);
}
- Добавим обработчик маршрута для получения капчи:
app.get("/get-captcha", async (req, res) => {
const userId = req.header("x-user-id");
if (!userId)
return res.status(400).json({ success: false, message: "Нет userId" });
try {
const passed = await redisClient.get(`captcha_${userId}`);
if (passed)
return res.json({ success: true, message: "Капча уже пройдена" });
const categories = ["vehicle", "animal", "nature"];
const category = categories[Math.floor(Math.random() * categories.length)];
const selected = getRandomItems(images, 9);
const correct = selected
.filter((img) => img.category === category)
.map((img) => img.src);
const captchaId = uuidv4();
await redisClient.setEx(
`captcha_data_${captchaId}`,
300,
JSON.stringify({ correct })
);
res.json({
success: false,
images: selected,
category,
captchaId,
});
} catch (err) {
console.error("Ошибка /get-captcha:", err);
res.status(500).json({ success: false, message: "Ошибка сервера" });
}
});
Что в этой части кода:
- Проверяется наличие userId в заголовке запроса. Если его нет, отправляется ошибка.
- Проверяется, прошёл ли пользователь капчу. Если да, отправляется сообщение об этом.
- Случайным образом выбирается категория (транспорт, животные или природа).
- Генерируются 9 случайных изображений, из которых выбираются правильные (по категории).
- Сохраняется информация о правильных изображениях в Redis с временным сроком 5 минут (300 секунд).
- Отправляется объект с изображениями, категорией и уникальным идентификатором капчи.
- Также добавим обработчик маршрута для проверки капчи:
app.post("/verify-captcha", async (req, res) => {
const userId = req.header("x-user-id");
const { captchaId, selectedImages } = req.body;
if (!userId || !captchaId || !Array.isArray(selectedImages)) {
return res.status(400).json({ success: false, message: "Неверные данные" });
}
try {
const raw = await redisClient.get(`captcha_data_${captchaId}`);
if (!raw)
return res.status(400).json({ success: false, message: "Истекла капча" });
const { correct } = JSON.parse(raw);
const isValid =
selectedImages.length === correct.length &&
selectedImages.every((img) => correct.includes(img));
if (isValid) {
await redisClient.setEx(`captcha_${userId}`, 3600, "passed");
}
res.json({
success: isValid,
message: isValid ? "Капча пройдена!" : "Неверно выбраны изображения!",
});
} catch (err) {
console.error("Ошибка /verify-captcha:", err);
res.status(500).json({ success: false, message: "Ошибка сервера" });
}
});
app.listen(PORT, () => {
console.log(`Сервер запущен на http://localhost:${PORT}`);
});
- Здесь мы получаем userId, captchaId и выбранные изображения из тела запроса.
- Проверяем, существует ли капча и не истекла ли она.
- Сравниваем выбранные изображения с правильными.
- Если капча пройдена, сохраняем статус в Redis на 1 час (3600 секунд).
- Отправляем результат проверки: успешное прохождение или ошибка.
- И наконец, запустим redis-server и server.js командой node server.js, откроем наш проект в браузере (http://localhost:3000). Протестируем:

У нас получилась настоящая полноценная капча с выбором картинок под соответствующую категорию! После того, как мы успешно пройдём капчу и обновим страницу, вместо капчи мы получим такое сообщение:

Наш “сайт” сохраняет состояние капчи, запоминает userId и держит эти данные в памяти 1 час, после чего userId обнуляется, и тогда пользователю придётся пройти капчу заново.
Созданные капчи не должны оставаться на одном месте, в них можно и нужно добавлять всё более широкий функционал. Вот чем можно усложнить и усовершенствовать свои системы:
- Конечно же, добавить больше изображений! Это усложнит и разнообразит капчу.
- Перемешивание изображений по координатам (а не просто src). Чтобы избежать брутфорса по URL, можно возвращать изображения без прямой ссылки на src, например, с base64 или хэшами.
- Валидация IP-адреса / user-agent / fingerprint: для минимальной защиты от автоматических попыток прохождения капчи с разных userId.
- Тайм-ауты между попытками
- Вместо captcha_${userId} выдать одноразовый captchaToken, который можно использовать при логине.
- Добавить подпись (signature) к captchaId, например, HMAC, чтобы защититься от подделки captchaId.
- Можно внедрять «ловушки» – похожие объекты, которые пользователь может перепутать.
- Docker-контейнеризация для более стабильного деплоя.
- HTTPS-only + SameSite cookies для защиты токенов: особенно важно, если потом подключить авторизацию.
- Логирование подозрительной активности: частые ошибки, подозрительно быстрые ответы и т.д.
- Возможность переключать язык (например, русский/английский).
Дополнительно подключать разные скрипты и скрытые элементы.
Какие ещё куки можно добавить:

Собственная капча – это гораздо больше, чем просто защита формы! Это упражнение, в котором сходятся вёрстка, клиентская логика, работа с сервером и хранением данных. Здесь есть всё: от интерфейса до архитектурных решений. Проект получается компактным, но насыщенным – особенно полезным для тех, кто хочет прокачаться во фронтенде и бэкенде одновременно. И, конечно, это отличный способ добавить что-то уникальное и полезное в свою работу. Надеемся, наш материал был полезен, желаем вам лёгкого обучения и удачных проектов!
Важно: Используйте CapMonster Cloud только для автоматизации и тестирования на своих сайтах или на ресурсах, к которым вы имеете законный доступ.