Como Criar Sua Própria CAPTCHA de Qualquer Tipo: O Guia Mais Detalhado
Se você usa a internet com frequência, provavelmente já teve que provar que é humano mais de uma vez. E agora você deve estar se perguntando: como posso criar minha própria CAPTCHA? Existem muitas soluções prontas por aí, mas desenvolver a sua própria não é apenas útil — também é muito interessante! Preparamos um guia completo com instruções claras e explicações passo a passo para ajudá-lo a criar sua própria proteção contra ciberataques. Confira e ganhe uma experiência valiosa no desenvolvimento de um sistema CAPTCHA poderoso e fácil de usar!
Neste artigo:
Se você está decidindo entre usar uma CAPTCHA personalizada ou uma solução pronta (como o reCAPTCHA do Google), é importante considerar a usabilidade, o nível de segurança e a complexidade da implementação. A maior vantagem das soluções personalizadas é que implementações únicas tornam o reconhecimento automatizado muito mais difícil. A tecnologia base pode ser semelhante, mas você sempre pode adicionar um toque pessoal.
Neste guia, vamos explicar como criar vários tipos de CAPTCHA. Aqui está um breve resumo:
CAPTCHA de Texto (digitar o texto a partir de uma imagem)
+ Simples e adequado para formulários pequenos.
- Facilmente burlável e pode ser irritante para os usuários.
CAPTCHA com Imagens (seleção em grade como o reCAPTCHA do Google)
+ Oferece proteção robusta, frequentemente usada em sistemas bancários e de votação.
- Pouco amigável em dispositivos móveis e pode ser difícil para alguns usuários.
CAPTCHA Deslizante
+ Rápido e prático para logins, especialmente em dispositivos móveis.
- Pode ser facilmente burlado por bots.
CAPTCHA de Quebra-Cabeça (arrastar uma peça para o lugar correto)
+ Interativo e ótimo para sites de jogos ou criativos.
- Demanda mais tempo para ser concluído.
Você também pode criar CAPTCHAs com áudio e verificação baseada em comportamento.
Se você decidiu criar sua própria CAPTCHA, precisará pensar bem na lógica central de como ela funciona. Você vai precisar tanto do frontend – a parte do lado do cliente que exibe a CAPTCHA no navegador – quanto do backend – a parte do lado do servidor que gera a CAPTCHA e verifica a resposta do usuário. Vamos dar uma olhada nas etapas gerais envolvidas:
Criar o layout do formulário da CAPTCHA – crie a interface com imagens, um controle deslizante ou campo de texto.
Gerar os dados da CAPTCHA no servidor – produza caracteres aleatórios, imagens ou peças de quebra-cabeça.
Exibir a CAPTCHA no lado do cliente – carregue os dados e adapte-os ao usuário.
Interação do usuário – rastreie cliques, movimentos e entradas.
Verificar a resposta do usuário no servidor – compare os dados enviados com a resposta correta.
A escolha da linguagem depende do tipo de CAPTCHA, da plataforma que você está utilizando e do nível de segurança desejado.

Para o lado do cliente (frontend), a implementação geralmente é feita usando HTML/CSS/JavaScript. Você pode optar por uma abordagem “vanilla” ou utilizar frameworks e bibliotecas como React.js (ou Vue, Svelte, Angular), Canvas/WebGL, e as seguintes bibliotecas:
dragula.js / interact.js – para funcionalidades de arrastar e soltar (úteis quando o usuário precisa clicar em um elemento, arrastá-lo e soltá-lo no local correto)
anime.js / GSAP – para animações
fabric.js / p5.js – para desenhar no canvas
CryptoJS – se precisar criptografar algo no lado do cliente (por exemplo, dados do CAPTCHA)

Para o lado do servidor (backend), você pode escolher praticamente qualquer linguagem de programação. Os principais requisitos são que ela:
Seja capaz de receber requisições do cliente (geralmente via HTTP)
Seja capaz de processar a lógica de validação do CAPTCHA
Retorne uma resposta (sucesso ou falha)
Aqui está um resumo geral de como funciona no servidor, independentemente da linguagem:
O cliente (navegador) envia o resultado do CAPTCHA (por exemplo, coordenadas do slider, token, texto, etc.)
O servidor verifica: a resposta é válida? O tempo limite expirou? O endereço IP é aceitável?
O servidor responde: "Sim, humano" ou "Não, bot"
Escolha a linguagem de programação com base nas suas preferências e nas tecnologias com as quais deseja trabalhar.

Nos nossos exemplos, usaremos a combinação de HTML/CSS/JavaScript + Node.js. Nossos sistemas de CAPTCHA serão sem frameworks, mas ao mesmo tempo máximo seguros e práticos. Por que escolhemos essa stack:
Node.js é excelente para APIs e lógica de backend
- Geração de tokens, verificação das respostas do CAPTCHA, armazenamento de estados de sessão, etc.
- Modelo assíncrono – ideal para lidar com múltiplas requisições de forma eficiente.
JavaScript no navegador – ideal para CAPTCHAs visuais/interativos
- Sliders, quebra-cabeças, cliques em imagens – tudo isso é facilmente implementado no frontend.
- Permite detectar comportamentos suspeitos: movimentos do mouse, tempos de resposta, eventos de focus/blur e muito mais.
Integração simples
- CAPTCHAs com essa stack são fáceis de integrar em qualquer formulário web.
- Além disso, é prático adicionar sessões, limites, proteção contra bots, etc.
Agora que temos a base teórica e escolhemos nossas ferramentas, podemos finalmente passar à prática!
Vamos construir uma versão básica de um CAPTCHA, que você poderá aprimorar e expandir com mais funcionalidades posteriormente.
- Crie uma pasta com qualquer nome (por exemplo, Textcaptcha), abra-a no seu editor de código e crie um arquivo chamado index.html. Aqui, você definirá os estilos para o CAPTCHA (pode colocá-los em um arquivo separado chamado styles.css e deixar a estrutura HTML no index.html, que descreveremos a seguir). Por exemplo, seus estilos podem ser assim:
<!DOCTYPE html>
<html lang="pt-BR">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>CAPTCHA de Texto</title>
<style>
/* Estilos para o corpo da página */
body {
font-family: Arial, sans-serif;
display: flex;
justify-content: center;
align-items: center;
height: 100vh;
background: #f4f4f4;
margin: 0;
}
/* Estilos para o contêiner do CAPTCHA */
.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;
}
/* Estilos para a imagem do CAPTCHA */
.captcha-image {
display: flex;
justify-content: center;
align-items: center;
margin-bottom: 15px;
border: 2px solid #ddd;
padding: 10px;
background: #fff;
border-radius: 5px;
}
/* Estilos para o campo de entrada do CAPTCHA */
.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;
}
/* Estilos para os botões (atualizar e enviar) */
.buttons {
display: flex;
justify-content: space-between;
align-items: center;
}
/* Estilos para o botão de atualizar CAPTCHA */
.refresh-button {
background: #007bff;
border: none;
padding: 5px;
border-radius: 50%;
cursor: pointer;
display: flex;
justify-content: center;
align-items: center;
}
/* Tamanho do ícone dentro do botão */
.refresh-button img {
width: 20px;
height: 20px;
}
/* Estilos para o botão de envio */
.submit-button {
background: #28a745;
border: none;
color: white;
padding: 10px 20px;
border-radius: 5px;
cursor: pointer;
}
/* Estilos para o botão quando está desabilitado */
.submit-button:disabled {
background: #ccc;
cursor: not-allowed;
}
/* Estilos para o botão ativo no hover */
.submit-button:hover:not(:disabled) {
background: #218838;
}
/* Estilos para a mensagem de sucesso do CAPTCHA */
.success-message {
color: green;
display: none;
font-weight: bold;
margin-top: 10px;
}
/* Estilos para a mensagem de erro do CAPTCHA */
.error-message {
color: red;
display: none;
font-weight: bold;
margin-top: 10px;
}
</style>
</head>
Vamos também adicionar o layout principal da página:
<body>
<form class="captcha-container" onsubmit="return false;">
<div class="captcha-image">
<img id="captchaImage" src="" alt="CAPTCHA" width="200" height="80">
</div>
<input type="text" id="captchaInput" class="captcha-input" placeholder="Digite o texto da imagem" 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="Atualizar">
</button>
<button type="submit" class="submit-button" id="submitButton" disabled>Verificar</button>
</div>
<p class="success-message" id="successMessage">CAPTCHA aprovado!</p>
<p class="error-message" id="errorMessage">Texto incorreto, tente novamente.</p>
</form>
<script src="script.js"></script>
</body>
</html>
Exibindo o CAPTCHA: Quando a página é carregada, o elemento <img id="captchaImage"> exibirá a imagem do CAPTCHA por meio de JavaScript.
Atualizando o CAPTCHA: O botão "Atualizar" chama uma função que atualiza a imagem do CAPTCHA (isso será tratado via JavaScript).
Verificando o CAPTCHA: Após o usuário digitar o texto no campo de entrada e clicar no botão "Verificar", o texto digitado é comparado com o da imagem. Se estiver correto, é exibida uma mensagem de sucesso; caso contrário, uma mensagem de erro é exibida.
Monitoramento de entrada: O botão "Verificar" só é ativado quando o usuário digita algo no campo de texto.
Toda a lógica do CAPTCHA está implementada em um arquivo JavaScript externo (script.js).
2. Crie outro arquivo chamado script.js — é onde o CAPTCHA será carregado e enviado para o servidor (local) para verificação. Quando a página carregar, o CAPTCHA será automaticamente obtido.
- O usuário digita o texto exibido na imagem do CAPTCHA.
- Quando o botão "Verificar" é clicado, uma requisição é enviada ao servidor para verificar a entrada.
- Se o texto estiver correto, uma mensagem de sucesso é exibida e o botão é desativado.
- Se o texto estiver incorreto, uma mensagem de erro é exibida e um novo CAPTCHA é carregado.
- O usuário também pode atualizar manualmente o CAPTCHA clicando no botão "Atualizar".
Vamos declarar uma variável captchaId para armazenar o identificador único do CAPTCHA. Esse ID será usado para verificar o texto digitado com o CAPTCHA específico no servidor:
let captchaId = "";
3. Agora vamos criar a função 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 loading CAPTCHA:", error));
}
Aqui, enviamos uma requisição GET ao servidor em http://localhost:3000/generate-captcha para obter uma nova imagem de CAPTCHA.
- .then((response) => response.json()): converte a resposta do servidor para o formato JSON.
- .then((data) => {...}): processa os dados recebidos:
- captchaId = data.captchaId: atribui à variável captchaId o valor retornado pelo servidor. Esse ID será usado para verificar a resposta.
- document.getElementById("captchaImage").src = data.captchaImage: define o atributo src da imagem com a URL recebida do servidor.
- .catch((error) => console.error("Erro ao carregar o CAPTCHA:", error)): se ocorrer um erro na requisição ou no processamento da resposta, ele será exibido no console.
4. Agora vamos adicionar um event listener para o botão de envio:
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("Erro ao verificar o CAPTCHA:", error));
});
- document.getElementById("submitButton").addEventListener("click", () => {...}): adiciona um ouvinte de evento ao botão "Verificar", que envia os dados digitados ao servidor para validação.
- const userInput = document.getElementById("captchaInput").value.trim();: obtém o valor inserido pelo usuário e remove espaços em branco das extremidades.
- if (!userInput) return;: se o campo estiver vazio, o código não continua.
- fetch("http://localhost:3000/verify-captcha", {...}): envia uma requisição POST ao servidor para verificar se o texto digitado corresponde ao CAPTCHA exibido.
A requisição inclui:
userInput: o texto digitado pelo usuário.
captchaId: o identificador do CAPTCHA recebido anteriormente.
.then((response) => response.json())
Processa a resposta do servidor (o resultado da verificação do CAPTCHA).
.then((result) => {...});
Se o CAPTCHA for verificado com sucesso (result.success):
Uma mensagem de sucesso é exibida.
O botão "Verificar" é desativado (submitButton.disabled = true).
Em caso de erro:
Uma mensagem de erro é exibida.
A função fetchCaptcha() é chamada para carregar uma nova imagem de CAPTCHA.
5. Agora, vamos adicionar um event listener para o botão de atualização do CAPTCHA:
document
.getElementById("refreshButton")
.addEventListener("click", fetchCaptcha);
Clicar nesse botão acionará a função fetchCaptcha(), que carrega uma nova imagem de CAPTCHA.
6. Event listener para o campo de entrada:
document.getElementById("captchaInput").addEventListener("input", (e) => {
document.getElementById("submitButton").disabled =
e.target.value.trim().length === 0;
});
fetchCaptcha();
A cada alteração de texto no campo de entrada (evento input):
- O comprimento do texto inserido é verificado.
- Se o texto estiver vazio, o botão "Verificar" é desativado (submitButton.disabled = true).
- Se o texto não estiver vazio, o botão "Verificar" é ativado (submitButton.disabled = false).
7. Ótimo — os estilos e o código do frontend para exibir e verificar o CAPTCHA estão prontos! Agora vamos passar para o lado do servidor, onde o CAPTCHA será gerado e a resposta do usuário será verificada.
Crie um arquivo chamado server.js, abra seu terminal e instale as seguintes dependências:
npm install express cors uuid canvas
O que vamos usar?
- express – um framework para Node.js que simplifica a criação de servidores web.
- cors – middleware para permitir requisições de outras origens (necessário para funcionar com o frontend).
- uuid – para gerar identificadores únicos (para gerar o captchaId).
- canvas – uma biblioteca para manipulação gráfica (para criar o CAPTCHA).
8. Agora vamos importar as dependências instaladas, criar uma instância do servidor e configurar o 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());
9. Vamos criar um armazenamento para o CAPTCHA. Declaramos o objeto captchaStore para armazenar dados como identificadores e texto:
const captchaStore = {};
10. Vamos gerar o texto do CAPTCHA. A função generateCaptchaText gera uma string aleatória com 6 caracteres (letras e números):
function generateCaptchaText() {
return Math.random().toString(36).substring(2, 8).toUpperCase();
}
11. Que tal adicionar uma cor aleatória? 😀 A função getRandomColor gera uma cor aleatória em formato 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})`;
}
12. Também vamos gerar uma fonte aleatória:
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; // Tamanho da fonte entre 30 e 40
return `${randomSize}px ${randomFont}`;
}
13. Vamos adicionar imagem com texto, ruído e distorções:
function generateCaptchaImage(text) {
const canvas = createCanvas(200, 80);
const ctx = canvas.getContext("2d");
// Fundo
ctx.fillStyle = "#f8f8f8";
ctx.fillRect(0, 0, canvas.width, canvas.height);
// Adiciona ruído (linhas)
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();
}
// Define fonte e cor do texto
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); // Inclinação leve
ctx.fillText(text, 0, 0);
return canvas.toDataURL(); // Retorna imagem CAPTCHA em base64
}
Agora vamos à API:
14. API para geração do CAPTCHA. Neste manipulador, geramos o CAPTCHA, armazenamos o texto e o ID, e enviamos os dados ao usuário:
app.get("/generate-captcha", (req, res) => {
const captchaText = generateCaptchaText();
const captchaId = uuidv4();
captchaStore[captchaId] = captchaText;
const captchaImage = generateCaptchaImage(captchaText);
res.json({ captchaId, captchaImage });
});
15. API para verificação do CAPTCHA. Neste manipulador, recebemos o texto digitado e comparamos com o valor armazenado:
app.post("/verify-captcha", (req, res) => {
const { userInput, captchaId } = req.body;
if (!captchaId || !captchaStore[captchaId]) {
return res
.status(400)
.json({ success: false, message: "CAPTCHA expirado ou não encontrado" });
}
const isCorrect = userInput.toUpperCase() === captchaStore[captchaId];
delete captchaStore[captchaId]; // Remove após verificação
res.json({
success: isCorrect,
message: isCorrect ? "CAPTCHA correto" : "CAPTCHA incorreto",
});
});
Aqui nós:
- Recebemos userInput (texto digitado) e captchaId.
- Verificamos se o captchaId existe no armazenamento.
- Se existir, comparamos os valores e retornamos o resultado (correto/incorreto).
16. Por fim, iniciamos nosso servidor na porta 3000!
app.listen(3000, () => console.log("Server is running on port 3000"));
No terminal, execute o servidor com o comando: node server.js e abra seu projeto no navegador. Viva! O CAPTCHA está criado e funcionando perfeitamente!

Existe também um tipo de CAPTCHA chamado slider, no qual você precisa mover um controle deslizante para uma determinada posição no formulário – por exemplo, até o lado direito. Vamos criá-lo nós mesmos!
- Crie uma nova pasta chamada "SimpleSlider" e, no seu editor de código, crie um arquivo index.html. Neste arquivo, vamos adicionar a estrutura principal da página, incluindo o contêiner do CAPTCHA, o slider e os botões. Também adicionaremos estilos para o slider, botões, mensagens de sucesso e erro, além de aplicar o CSS para estilizar a página e centralizar os elementos.
<!DOCTYPE html>
<html lang="pt">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Slider Simples</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>Arraste o controle deslizante até a direita</p>
<div class="slider" id="slider">
<div class="slider-button" id="sliderButton"></div>
</div>
<p class="success-message" id="successMessage">CAPTCHA verificado com sucesso!</p>
<p class="error-message" id="errorMessage">Erro, tente novamente.</p>
</form>
<script src="script.js"></script>
</body>
</html>
2. Agora vamos criar um arquivo JavaScript chamado 2script.js, que lidará com a funcionalidade do slider, o arrasto e a verificação com o servidor. Vamos criar uma variável captchaSession, que armazenará o ID da sessão atual do captcha recebido do servidor, para que possamos usá-lo posteriormente na validação:
let captchaSession = "";
3. Carregando a sessão do captcha do servidor.
function fetchCaptchaSession() {
fetch("http://localhost:3000/generate-slider-captcha")
.then((response) => response.json())
.then((data) => {
captchaSession = data.sessionId;
})
.catch((error) => console.error("Erro ao carregar o captcha:", error));
}
Aqui fazemos o seguinte:
- Enviamos uma requisição para http://localhost:3000/generate-slider-captcha.
- Esperamos uma resposta JSON com o campo sessionId.
- Armazenamos esse sessionId na variável captchaSession.
- Em caso de erro, registramos uma mensagem no console.
Isso é necessário para que depois possamos enviar o sessionId para verificação em /verify-slider.
4. Em seguida, acessamos os elementos HTML necessários:
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 – indica se o modo de arrasto está ativo.
- sliderCompleted – prevents repeated actions if the captcha has already been passed.
5. Agora vamos tratar o clique do mouse no botão deslizante:
button.addEventListener("mousedown", () => {
if (sliderCompleted) return;
isDragging = true;
});
Quando o botão do mouse é pressionado sobre o slider, verificamos se o usuário já completou o captcha. Se não, ativamos o modo de arrasto (isDragging = true).
6. Movimentação do controle deslizante:
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";
});
Quando o mouse se move, verificamos se o modo de arrasto está ativado, calculamos a posição do mouse em relação ao slider e limitamos os valores dentro das bordas. Depois, ajustamos a propriedade left do botão para movê-lo.
7. Finalizamos a ação quando o botão do mouse é solto:
document.addEventListener("mouseup", () => {
if (!isDragging) return;
isDragging = false;
let rect = slider.getBoundingClientRect();
let finalPosition = parseInt(button.style.left);
Verificamos se o arrasto ocorreu, desativamos o modo isDragging e obtemos a posição final do slider. Em seguida, verificamos se o slider chegou ao final:
if (finalPosition >= rect.width - button.offsetWidth - 5) {
8. Enviando ao servidor (execução bem-sucedida):
fetch("http://localhost:3000/verify-slider", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ sessionId: captchaSession, completed: true }),
})
Enviamos uma requisição POST para /verify-slider. No corpo da requisição, incluímos:
- sessionId – o ID da sessão
- completed: true – o usuário moveu o slider até o final.
9. Tratamento da resposta:
Se o servidor responder com success: true:
- Exibimos uma mensagem de sucesso.
- Mudamos a cor do botão.
- Marcamos o captcha como completo.
Se o servidor responder com erro:
- Exibimos uma mensagem de erro.
- Resetamos o slider.
- Carregamos um novo captcha.
.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();
}
})
10. Podemos também adicionar tratamento de erro para a requisição:
.catch((error) => {
console.error("Erro durante a verificação do captcha:", error);
errorMessage.style.display = "block";
button.style.left = "0px";
fetchCaptchaSession();
});
11. Se o slider não chegou ao fim, e o usuário não o completou, ele é resetado para a esquerda:
} else {
button.style.left = "0px";
}
});
fetchCaptchaSession();
12. Agora vamos para a parte do servidor. Criaremos um arquivo chamado server.js no mesmo diretório, abriremos o terminal e instalaremos as dependências necessárias:
npm install express cors body-parser
- express é necessário para executar o servidor HTTP.
- cors permite requisições entre origens diferentes.
- body-parser é usado para processar o corpo JSON de requisições POST.
13. Vamos importar esses pacotes no nosso projeto:
import express from "express";
import cors from "cors";
import bodyParser from "body-parser";
14. Agora vamos configurar o servidor. Criaremos uma instância do aplicativo Express e definiremos a porta (por exemplo, 3000):
const app = express();
const PORT = 3000;
15. Permitiremos requisições de outros domínios, configuraremos o Express para lidar com JSON no corpo da requisição e definiremos o armazenamento das sessões do captcha: usaremos um Map para armazenar sessões temporárias (no futuro, o Redis pode ser uma escolha melhor). SESSION_TTL define o tempo de vida da sessão: 5 minutos.
const sliderSessions = new Map();
const SESSION_TTL = 5 * 60 * 1000;
16. Vamos limpar sessões expiradas. A cada minuto, uma verificação será executada para remover sessões com mais de 5 minutos:
function cleanUpSessions() {
const now = Date.now();
for (const [sessionId, { timestamp }] of sliderSessions) {
if (now - timestamp > SESSION_TTL) {
sliderSessions.delete(sessionId);
}
}
}
setInterval(cleanUpSessions, 60 * 1000); // Executa a cada minuto
17. Criando um novo CAPTCHA. Geramos um sessionId, que é armazenado na memória junto com o timestamp atual. O cliente recebe esse sessionId, que será usado para a verificação posterior:
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(`Nova sessão: ${sessionId}`);
res.json({ sessionId });
});
18. Verificação do CAPTCHA e inicialização do servidor. Recebemos o sessionId e completed (um valor booleano) do corpo da requisição.
- Se a sessão não for encontrada – retornamos um erro.
- Se o usuário completou corretamente o slider – atualizamos a sessão e retornamos uma resposta de sucesso:
app.post("/verify-slider", (req, res) => {
const { sessionId, completed } = req.body;
if (!sessionId || !sliderSessions.has(sessionId)) {
return res
.status(400)
.json({ success: false, message: "Sessão não encontrada" });
}
if (completed) {
sliderSessions.set(sessionId, { completed: true, timestamp: Date.now() });
console.log(`Captcha validado: ${sessionId}`);
return res.json({ success: true });
} else {
return res
.status(400)
.json({ success: false, message: "Captcha não completado" });
}
});
app.listen(PORT, () => {
console.log(`Servidor rodando em http://localhost:${PORT}`);
});
19. Inicie o servidor executando o comando node server.js no terminal e abra o projeto no navegador. Pronto — um CAPTCHA de slider simples e funcional!

Se você simplesmente armazenar tudo na memória – será rápido, mas não confiável: se o servidor cair, tudo será perdido. Por isso, é melhor usar o Redis para cache de dados – ele é rápido, trabalha com TTL e é ideal para dados temporários como CAPTCHAs e tokens.
Um esquema aproximado seria assim: você gera o CAPTCHA – armazena no Redis com um ID – envia esse ID ao usuário via cookie ou parâmetro de URL – o usuário resolve – você verifica e emite (ou não) um token. O token também é armazenado no Redis para futura verificação no login ou envio de formulário.
Os cookies servem para lembrar: "Este usuário já passou pelo CAPTCHA". Você pode configurar o cookie para durar 10 minutos ou 1 hora – como preferir. Após esse tempo, o CAPTCHA será exibido novamente para o visitante do site.
Também é possível adicionar temporizadores para evitar spam, limites por IP e outras automações úteis. O Redis faz tudo isso muito bem.
O Redis é excelente para armazenamento temporário, mas às vezes é necessário manter estatísticas por mais tempo: quantas vezes alguém resolveu, de onde veio, com que frequência errou, qual tipo de CAPTCHA foi usado, etc. Nesses casos, vale a pena conectar um banco de dados – Postgres, Mongo ou até mesmo MySQL.
O banco de dados é necessário para logs, análises e monitoramento de atividades suspeitas.
Parâmetros adicionais:
Além do captchaId, geralmente são utilizados os seguintes parâmetros para CAPTCHAs:

O que fazer com isso?
Adicione esses campos ao objeto ao gerar o CAPTCHA e envie-os para o Redis e também para o banco de dados. Ao verificar o CAPTCHA, atualize o status (sucesso/fracasso) e incremente o número de tentativas. Em caso de sucesso, você pode até salvar imediatamente um token de autenticação no Redis, vinculado ao captchaId ou ao IP. Se tudo for bem configurado, você terá não apenas uma proteção, mas um sistema completo de controle anti-bot e, se desejar, um painel de estatísticas onde será possível ver quem, onde, quando e como resolveu os CAPTCHAs.
Para proteger ainda mais o CAPTCHA, também é possível adicionar:
- Ruído, distorções, parâmetros aleatórios (como fizemos no CAPTCHA de texto).
- Análise do movimento do mouse (para CAPTCHA de arrastar/bloco).
- Ofuscação e criptografia.
Não envie coordenadas ou hashes para o cliente. Assine os dados (por exemplo, usando JWT).
Vamos da teoria à prática! Imagine um projeto onde, para entrar no sistema, o usuário precisa não apenas inserir login e senha, mas também resolver um CAPTCHA em forma de quebra-cabeça. Nesse CAPTCHA, o usuário deve arrastar uma peça da imagem (que será recortada diretamente no nosso projeto) até o local correto sobre um fundo. O servidor verifica o token do CAPTCHA e, somente se o CAPTCHA for resolvido corretamente e os dados de login/senha forem validados, o usuário será autenticado com sucesso. Vamos à implementação dessa ideia!
1. Primeiro, vamos preparar todas as ferramentas e arquivos necessários. Crie uma pasta chamada Puzzleslider e abra-a no seu editor de código. Agora adicione uma imagem de fundo do CAPTCHA com tamanho de 320x180 pixels na pasta images. Esta será a base do nosso quebra-cabeça.
2. Queremos armazenar e gerenciar sessões e tokens usando o Redis, então instale-o no seu computador e inicie o servidor Redis (você pode encontrar todas as instruções no site oficial).
Servidor Redis iniciado no Windows:

3. Agora, na pasta Puzzleslider, vamos criar o arquivo index.html e adicionar os estilos com CSS embutido e a estrutura da página:
<!DOCTYPE html>
<html lang="pt">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Captcha + Autenticação</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>Verificação de Captcha</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>Login</h3>
<form id="loginForm">
<input type="text" id="username" placeholder="Nome de usuário" />
<input type="password" id="password" placeholder="Senha" />
<button id="loginButton" type="submit">Entrar</button>
<p class="error" id="loginStatus"></p>
</form>
</div>
<script src="script.js"></script>
</body>
</html>
Este código cria uma página com dois blocos principais: um formulário de captcha e um formulário de autenticação. O primeiro bloco contém o captcha com dois elementos canvas para a imagem e sua peça separada, além de um controle deslizante para verificar a solução. O segundo bloco tem campos para nome de usuário e senha, com um botão "Entrar" para autenticação. A interação será tratada em JavaScript via o arquivo externo script.js.
Aqui, todo o processo será dividido em três partes principais:
- Geração e verificação do captcha.
- Movimento do controle deslizante (slider) e verificação da posição.
- Envio dos dados do formulário de autenticação após a verificação bem-sucedida do captcha.
4. Vamos implementar a funcionalidade do captcha com quebra-cabeça (puzzle) e um formulário de autenticação seguro. Para isso, criaremos um novo arquivo chamado script.js e inicializaremos as variáveis:
let sessionId = "";
let cutoutX = 0;
let cutoutY = 0;
let isDragging = false;
const puzzleWidth = 320;
const puzzleHeight = 180;
const pieceSize = 50;
- sessionId: armazenará o identificador único da sessão, usado para validar o captcha no servidor.
- cutoutX, cutoutY: coordenadas da parte da imagem que será recortada e transformada em peça do quebra-cabeça.
- isDragging: flag para acompanhar se o usuário está arrastando o controle deslizante.
- puzzleWidth, puzzleHeight, pieceSize: definem as dimensões da imagem do captcha e do tamanho da peça.
5. Agora vamos obter os elementos do HTML com os quais o JavaScript irá interagir:
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 e puzzlePieceCanvas: elementos <canvas> usados para exibir a imagem e a peça recortada.
- slider e sliderButton: elementos do controle deslizante que o usuário vai arrastar.
- captchaStatus: elemento para exibir mensagens de status do captcha.
- loginButton: botão para envio do formulário de login.
6. Criamos agora a função que requisita um novo captcha:
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("Erro no captcha:", err));
}
A função fetchCaptcha() faz uma requisição para o servidor local (http://localhost:3000/generate-puzzle-captcha) para obter os dados necessários para gerar um novo captcha.
O servidor retorna os dados:
- sessionId: identificador da sessão.
- cutoutX, cutoutY: coordenadas da parte que será recortada da imagem.
- backgroundImage: URL da imagem de fundo.
A imagem é carregada, e quando estiver pronta, a função drawCaptcha será chamada para desenhar o captcha no canvas.
A chamada resetState() reinicia o estado da interface.
7. Desenhando o captcha e recortando a peça do quebra-cabeça:
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);
}
- No puzzleCanvas é desenhada a imagem completa.
- No puzzlePieceCanvas é desenhado apenas o fragmento recortado (com tamanho pieceSize).
- Em seguida, o pedaço da imagem nas coordenadas cutoutX e cutoutY é removido do fundo da captcha.
8. Resetando o estado do captcha — o slider e o pedaço da imagem voltam à posição inicial. O botão de login é desativado até que o captcha seja resolvido com sucesso:
function resetState() {
sliderButton.style.left = "0px";
puzzlePieceCanvas.style.left = "0px";
puzzlePieceCanvas.style.top = `${cutoutY}px`;
captchaStatus.textContent = "";
loginButton.disabled = true;
}
9. Tratando o início do arrasto do controle deslizante:
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 = "Captcha resolvido ✅";
captchaStatus.style.color = "green";
loginButton.disabled = false;
} else {
captchaStatus.textContent = "Incorreto. Tente novamente.";
captchaStatus.style.color = "red";
setTimeout(fetchCaptcha, 1000);
}
});
};
document.addEventListener("mousemove", move);
document.addEventListener("mouseup", stop);
});
- Quando o usuário pressiona o botão do slider (mousedown), começa o processo de arrasto.
- O movimento do mouse (mousemove) é monitorado e o slider e a peça do quebra-cabeça se movem juntos.
- Ao soltar o botão do mouse (mouseup), a posição do slider é verificada e os dados são enviados ao servidor para validar o captcha.
10. Login:
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 = "Login bem-sucedido!";
status.style.color = "green";
} else {
status.textContent = "Erro: " + data.message;
status.style.color = "red";
}
})
.catch((err) => {
const status = document.getElementById("loginStatus");
status.textContent = "Erro ao tentar fazer login.";
status.style.color = "red";
});
});
fetchCaptcha();
- Quando o usuário clica no botão de login, os dados (nome de usuário, senha e token do captcha) são enviados ao servidor.
- Se o login for bem-sucedido, uma mensagem de sucesso é exibida. Caso contrário, uma mensagem de erro é mostrada.
11. Os estilos para o CAPTCHA e toda a parte cliente necessária já estão prontos – agora podemos passar para o servidor, que será responsável por armazenar os dados da sessão e os tokens, gerar o CAPTCHA, validá-lo e autorizar os usuários com verificação obrigatória do CAPTCHA por meio do token. Vamos criar um novo arquivo server.js, abrir o terminal e instalar todas as ferramentas necessárias:
npm install express cors body-parser @redis/client uuid
- express: framework para trabalhar com o servidor.
- cors: módulo para configurar o CORS (Cross-Origin Resource Sharing), permitindo que o servidor aceite requisições de outros domínios.
- body-parser: para fazer o parsing de JSON nas requisições.
- @redis/client: biblioteca Redis para interação com o banco de dados Redis (que já instalamos e iniciamos com redis-server).
- uuid: para criar sessões e tokens únicos.
12. Importamos as dependências e configuramos o servidor:
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() – cria uma instância do aplicativo Express.
- port = 3000 – define a porta na qual o servidor irá rodar.
- app.use(cors()) – habilita CORS, permitindo requisições de outros domínios.
- app.use(bodyParser.json()) – configura o servidor para interpretar JSON no corpo das requisições.
- app.use(express.static("images")) – permite que o Express sirva arquivos estáticos da pasta "images" (vamos usar uma imagem de 320x180).
13. Conectamos ao Redis:
const redisClient = createClient();
redisClient.on("error", (err) => console.log("Erro no Redis:", err));
redisClient.connect();
14. Gerando e verificando o CAPTCHA:
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 });
}
});
Aqui criamos a rota POST /verify-puzzle, que verifica se o usuário resolveu corretamente o CAPTCHA.
- sessionId e position (posição do controle deslizante) são enviados no corpo da requisição.
- const session = await redisClient.get(sessionId) – recupera os dados da sessão no Redis. Se não existir, retorna erro 400.
- const { cutoutX } = JSON.parse(session) – extrai a coordenada da peça do quebra-cabeça.
- Verifica-se se a posição do usuário está dentro de uma margem de erro de 10 pixels. Se sim, o CAPTCHA é considerado resolvido.
- Se for resolvido, um token é gerado e salvo no Redis, com validade de 300 segundos, e enviado ao cliente.
15. Autenticação com verificação do CAPTCHA:
app.post("/login", async (req, res) => {
const { username, password, captchaToken } = req.body;
if (!captchaToken) {
return res.status(400).json({ success: false, message: "Token de CAPTCHA ausente" });
}
// Verifica a validade do token de CAPTCHA
const validCaptcha = await redisClient.get(`captcha-token:${captchaToken}`);
if (!validCaptcha) {
return res.status(403).json({ success: false, message: "CAPTCHA não resolvido" });
}
// Exemplo de verificação
if (username === "test" && password === "password") {
res.json({ success: true });
} else {
res.status(401).json({ success: false, message: "Credenciais incorretas" });
}
});
app.listen(port, () => console.log(`Servidor rodando em http://localhost:${port}`));
Aqui criamos o POST /login, que processa a requisição de login.
Verificamos se o captchaToken está presente no corpo da requisição. Se não estiver, retorna erro 400.
const validCaptcha = await redisClient.get(...) – verifica se o token existe no Redis.
Se não existir, significa que o CAPTCHA não foi resolvido, e o acesso é negado com erro 403.
Se o CAPTCHA for válido, o servidor verifica o nome de usuário e a senha. No exemplo, os dados esperados são "test" e "password".
Se estiverem corretos, o servidor retorna sucesso. Caso contrário, erro 401.
Para visualizar todas as sessões e chaves armazenadas no Redis, você pode usar o comando KEYS *.
Por exemplo:

16. Vamos verificar se o servidor Redis está em execução e funcionando corretamente: execute o server.js com o comando node server.js e abra o projeto no navegador.
Vamos testar o formulário de login e o captcha — se tudo estiver correto (resolvemos o captcha e inserimos os dados certos), veremos o seguinte resultado:

Não conseguiremos fazer login até que resolvamos o captcha e digitemos o nome de usuário e a senha corretos.

Nosso código está funcionando perfeitamente! Mas podemos ir além e criar uma solução na qual o usuário não precise resolver o captcha toda vez que acessar o sistema. Tudo isso pode ser implementado adicionando o uso de cookies no navegador do usuário.
Sugerimos que você analise o exemplo a seguir e tente desenvolver um captcha completo com armazenamento de tokens e sessões, além do uso de cookies:
Vamos escrever um código que criará um sistema de captcha com imagens, onde o usuário precisará selecionar as imagens que pertencem a uma determinada categoria — por exemplo, carros, animais ou natureza.
Quando o usuário acessar a página, ele receberá um conjunto de imagens e deverá escolher aquelas que correspondem à categoria indicada. O servidor irá gerar o captcha selecionando imagens aleatórias e criando um código único para verificação.
Depois que o usuário fizer sua escolha e clicar no botão, o resultado será enviado ao servidor para verificar se as imagens foram escolhidas corretamente.
Para evitar que o usuário precise resolver o captcha novamente — caso ele já o tenha resolvido — os dados serão armazenados no Redis, e usaremos cookies para rastrear se o captcha já foi validado.
Sim, o trabalho será um pouco mais complexo do que nos exemplos anteriores, mas o resultado valerá a pena!
Vamos começar com a preparação:
1. Para começar, crie uma pasta com qualquer nome que preferir (no nosso exemplo, usaremos Gridcaptcha). Dentro dela, crie uma subpasta chamada images e adicione algumas imagens com tamanho 150x150 ou 100x100 pixels. Para começar, o ideal é ter pelo menos 9 imagens, mas quanto mais você tiver, mais variada e desafiadora será a captcha!
As imagens devem ser organizadas por categorias. Por exemplo: vehicle, animal e nature. E devem ter nomes coerentes, como car.jpg, cat.jpg, mountain.jpg, etc.
2. Instale o Redis, como feito no exemplo anterior.
3. Também podemos já criar o arquivo server.js e adicionar todas as dependências necessárias com o seguinte comando:
npm install express cors body-parser morgan @redis/client uuid
Explicação das dependências:
- express – usado para criar o servidor e as rotas (por exemplo, para lidar com as requisições /get-captcha e /verify-captcha).
- cors – middleware que permite requisições entre diferentes domínios.
- body-parser – middleware que faz o parsing dos dados enviados nas requisições (especialmente útil para o /verify-captcha).
- morgan – middleware de logging usado com o nível dev, que exibe informações resumidas sobre as requisições no console.
- @redis/client – cliente para conectar ao Redis, armazenar os dados do captcha e verificar se o usuário já passou pela verificação.
- uuid – biblioteca para gerar identificadores únicos para os captchas e os usuários, possibilitando o rastreamento de sessões e dados associados.
4. Vamos criar o arquivo index.html e adicionar a estrutura e a aparência da nossa futura captcha com imagens:
<!DOCTYPE html>
<html lang="pt">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Captcha com imagens</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">Carregando captcha...</p>
<div class="captcha-images" id="captchaImages"></div>
<button class="submit-button" id="submitButton" disabled>Verificar</button>
<p class="success-message" id="successMessage">Captcha verificada com sucesso!</p>
<p class="error-message" id="errorMessage">Imagens incorretas selecionadas! Tente novamente.</p>
</div>
<script src="script.js"></script>
</body>
</html>
Vamos exibir a captcha em forma de grade 3x3 com imagens de 100x100 pixels. Quando o usuário seleciona uma imagem, ela aumenta de tamanho e recebe uma borda azul. O botão "Verificar" fica desativado no início, mas é ativado após a seleção de imagens — seu destaque é feito com bordas azuis e texto branco. As mensagens de sucesso ou erro são exibidas, respectivamente, em verde e vermelho.
5. No novo arquivo script.js, vamos criar algumas variáveis e obter a captcha do servidor com a função 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 é uma coleção que armazenará as imagens selecionadas.
captchaId é o ID da captcha atual, que será usado para a verificação.
userId é o identificador do usuário, que é salvo no localStorage – também seria possível usar cookies, mas neste caso não é necessário, pois o userId é usado apenas no lado do cliente e não é enviado com cada requisição ao servidor, como no caso de autenticação. Se o userId não existir, geramos um novo ID com crypto.randomUUID() e o salvamos no localStorage.
6. Carregar captcha do servidor
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 =
"Captcha já verificada!";
document.getElementById("captchaImages").innerHTML = "";
document.getElementById("submitButton").style.display = "none";
return;
}
captchaId = data.captchaId;
document.getElementById("captchaInstruction").textContent =
`Selecione todas as imagens com ${data.category}`;
renderImages(data.images);
} catch (error) {
console.error("Erro ao carregar captcha:", error);
alert("Ocorreu um erro. Tente novamente.");
}
}
A função fetchCaptcha faz uma requisição ao servidor para obter a captcha. A requisição contém o cabeçalho com o userId.
O servidor retorna um objeto JSON com informações sobre se o usuário já completou a captcha (data.success).
Se a captcha já tiver sido verificada, será exibida uma mensagem e os elementos com imagens e botão serão ocultados.
Se a captcha não tiver sido completada, o captchaId e a categoria de imagens serão armazenados, e a função renderImages será chamada para exibir as imagens ao usuário.
7. Exibir e processar seleção de imagens
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 recebe um array de imagens e as exibe no contêiner com o ID captchaImages.
Para cada imagem, é criado um elemento <img> com atributos src e data-index, além de um manipulador de eventos de clique.
Ao selecionar uma imagem, ela é adicionada à coleção selectedImages. Enquanto nenhuma imagem estiver selecionada, o botão de envio (submitButton) permanece desativado.
Quando o usuário clica em uma imagem, ela é adicionada ou removida da coleção selectedImages.
Visualmente, isso se reflete na adição ou remoção da classe selected, que aumenta a escala da imagem e muda a borda para azul.
O botão de envio é ativado apenas se houver pelo menos uma imagem selecionada.
8. Enviar imagens selecionadas ao servidor
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("Erro ao enviar captcha:", error);
alert("Erro do servidor. Tente novamente mais tarde.");
}
});
Quando o usuário clica no botão "Verificar", enviamos uma requisição POST ao servidor no endpoint /verify-captcha, contendo o captchaId e o array com as imagens selecionadas.
Com base na resposta do servidor (um JSON com o campo success), exibimos uma mensagem de sucesso ou erro.
Após a verificação da captcha, a função setTimeout chama fetchCaptcha novamente após 1,5 segundos, para atualizar a captcha.
9. Vamos voltar ao arquivo previamente criado server.js e importar as dependências já instaladas:
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";
10. Conectando-se ao Redis:
const app = express();
const PORT = 3000;
const redisClient = createClient();
redisClient.connect().catch((err) => console.error("Erro no Redis:", err));
Aqui:
- É criado um objeto da aplicação Express (app).
- Define-se a porta na qual o servidor vai escutar (3000).
- Cria-se um cliente Redis e inicia-se a conexão.
11. Configurando os middlewares:
app.use(cors({ origin: true, credentials: true }));
app.use(bodyParser.json());
app.use(morgan("dev"));
- cors: permite acesso a partir de qualquer origem e habilita o envio de cookies.
- bodyParser: faz o parsing do corpo das requisições no formato JSON.
- morgan: faz o log das requisições no console no formato "dev".
12. Tornando os arquivos da pasta images acessíveis via URL /images:
app.use("/images", express.static("images"));
13. Definimos um array de imagens: cada objeto contém o caminho da imagem e sua categoria — no futuro, será possível expandir os metadados e adicionar/recuperar imagens de um banco de dados.
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" },
];
14. Selecionando aleatoriamente uma quantidade definida de imagens do array:
function getRandomItems(arr, count) {
return [...arr].sort(() => 0.5 - Math.random()).slice(0, count);
}
15. Adicione um manipulador de rota para buscar o captcha:
app.get("/get-captcha", async (req, res) => {
const userId = req.header("x-user-id");
if (!userId)
return res.status(400).json({ success: false, message: "No userId" });
try {
const passed = await redisClient.get(`captcha_${userId}`);
if (passed)
return res.json({ success: true, message: "Captcha already passed" });
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("Error in /get-captcha:", err);
res.status(500).json({ success: false, message: "Server error" });
}
});
O que esta parte do código faz:
- Verifica a presença do userId no cabeçalho da requisição. Se estiver ausente, retorna um erro.
- Verifica se o usuário já passou pelo captcha. Se sim, envia uma mensagem informando isso.
- Uma categoria (veículos, animais ou natureza) é escolhida aleatoriamente.
- Nove imagens aleatórias são geradas, das quais as corretas (que correspondem à categoria) são identificadas.
- As informações sobre as imagens corretas são salvas no Redis com expiração de 5 minutos (300 segundos).
- Um objeto contendo as imagens, a categoria selecionada e um ID único do captcha é enviado na resposta.
16. Agora, vamos adicionar um manipulador de rota para a verificação do captcha:
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: "Invalid data" });
}
try {
const raw = await redisClient.get(`captcha_data_${captchaId}`);
if (!raw)
return res.status(400).json({ success: false, message: "Captcha expired" });
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 ? "Captcha passed!" : "Incorrect images selected!",
});
} catch (err) {
console.error("Error in /verify-captcha:", err);
res.status(500).json({ success: false, message: "Server error" });
}
});
app.listen(PORT, () => {
console.log(`Server running at http://localhost:${PORT}`);
});
Aqui, recuperamos o userId, o captchaId e as imagens selecionadas do corpo da requisição.
- Verificamos se o captcha ainda existe e não expirou.
- Em seguida, comparamos as imagens selecionadas com as corretas.
- Se o captcha for resolvido corretamente, salvamos o status de sucesso no Redis por 1 hora (3600 segundos).
- Finalmente, enviamos o resultado da verificação: sucesso ou erro.
17. E por fim, inicie o redis-server e execute server.js com o comando: node server.js. Depois abra o projeto no seu navegador em: http://localhost:3000.
Hora de testar!

Nós criamos um CAPTCHA totalmente funcional com seleção de imagens baseada em uma categoria específica!
Após resolver o CAPTCHA com sucesso e atualizar a página, veremos a seguinte mensagem em vez do CAPTCHA:

Nosso “site” preserva o estado do CAPTCHA lembrando o userId e armazenando esses dados na memória por 1 hora. Após esse período, o userId é redefinido e o usuário precisará resolver o CAPTCHA novamente.
Os CAPTCHAs criados não devem permanecer estáticos — há muito espaço para crescimento e aumento de complexidade. Aqui estão algumas ideias para melhorar e expandir seu sistema:
- Adicione mais imagens! Isso aumentará a complexidade e a diversidade do CAPTCHA.
- Embaralhe as coordenadas das imagens (não apenas os links src). Para evitar ataques de força bruta usando URLs das imagens, você pode retornar as imagens em formato base64 ou usar hashes em vez de links diretos.
- Valide o endereço IP / user-agent / fingerprint: adiciona uma camada básica de proteção contra tentativas automatizadas de CAPTCHA usando diferentes userIds.
- Defina timeouts entre tentativas para desacelerar as tentativas repetidas de adivinhação.
- Emita um captchaToken de uso único em vez de captcha_${userId}, que pode ser usado durante login ou outras ações seguras.
- Adicione uma assinatura (HMAC) ao captchaId para evitar adulterações.
- Inclua imagens “armadilhas” — visualmente semelhantes, mas incorretas — para aumentar o desafio.
- Containerização com Docker para uma implantação mais estável.
- Somente HTTPS + cookies com atributo SameSite para proteger os tokens — especialmente importante se futuramente houver integração com autenticação.
- Registro de atividades suspeitas, como erros repetidos ou respostas anormalmente rápidas.
- Suporte a mudança de idioma (ex: russo/inglês) para maior acessibilidade.
- Inclusão de scripts adicionais ou elementos ocultos para dificultar que bots compreendam o funcionamento do CAPTCHA.
Cookies adicionais que você pode adicionar:

Criar seu próprio CAPTCHA é muito mais do que apenas proteger um formulário! É um exercício que reúne design de layout, lógica no lado do cliente, desenvolvimento no lado do servidor e armazenamento de dados. Ele abrange tudo — da interface do usuário até decisões arquiteturais. O resultado é um projeto compacto, mas rico, especialmente valioso para quem deseja aprimorar simultaneamente as habilidades de frontend e backend. E, claro, é uma ótima maneira de adicionar algo único e útil ao seu portfólio.
Esperamos que este guia tenha sido útil — desejamos a você um aprendizado tranquilo e projetos de sucesso!
Aviso: Lembre-se de que o produto deve ser utilizado apenas para automação e testes em seus próprios sites ou em sites para os quais você possua permissão legal de acesso.