如何创建自己的任意类型验证码:最详尽的指南 第一部分
如果你经常使用互联网,那么你肯定不止一次向网站证明过自己是人类。现在你可能在思考:该如何自己创建验证码?虽然现成的解决方案很多,但亲手制作一个验证码不仅实用,还非常有趣!我们为你准备了一份扩展版的详细教程,提供清晰的步骤,帮助你构建自己的防护系统,抵御网络攻击。欢迎你阅读这篇文章,获得一次有趣的开发体验,打造一个既方便又安全的验证码系统!
文章目录:
如果你正在决定使用哪种验证码——自己制作的,还是现成的(例如 Google 的 reCAPTCHA)——你需要考虑使用的便利性、安全性级别以及实现的复杂性。最主要的优势在于:独特的验证码实现能极大地增加自动识别的难度。是的,技术可能大致相同,但你总可以加入自己的特色。在本指南中,我们将介绍几种验证码的创建方式。以下是它们的简要介绍:
文本验证码(从图片中输入文字)
+ 简单,适用于小型表单。
- 容易被破解,可能让用户感到烦躁。
图像验证码(网格、选择特定图片,类似 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 – 实现拖放操作(例如用户用鼠标点击一个元素,拖动并放置到指定位置)
- anime.js / GSAP – 用于制作动画效果
- fabric.js / p5.js – 用于在 canvas 上绘图
- CryptoJS – 如果需要在客户端进行加密(例如加密验证码数据)

对于服务器端(后端),其实可以选择任何一种编程语言。最关键的是它必须具备以下能力:
- 能够接收来自客户端的请求(通常是 HTTP 请求)
- 能够处理验证码的验证逻辑
- 能返回响应(验证成功或失败)
无论使用哪种语言,服务器上的处理流程大致如下:
- 客户端(浏览器)发送验证码验证结果(例如滑块坐标、令牌、输入的文本等)
- 服务器进行验证:结果是否有效?时间是否过期?IP 地址是否允许?
- 服务器返回响应:“是,人类”或“不是,是机器人”
你可以根据自己的使用习惯和偏好的技术栈选择编程语言:

在我们的示例中,我们将使用 HTML/CSS/JavaScript + Node.js 的组合。我们的验证码系统不会使用框架,但会尽可能做到安全且易于使用。我们选择这套技术栈的原因如下:
Node.js 非常适合处理 API 和业务逻辑
- 用于生成令牌、验证验证码结果、存储会话状态等功能
- 异步模型方便处理大量请求
浏览器中的 JavaScript 非常适合可视化/交互式验证码
- 滑块、拼图、图像点击等功能都可以轻松在前端实现
- 还可以捕捉可疑行为:鼠标移动、操作时机、页面 focus/blur 等等
易于集成
- 使用这套技术栈构建的验证码可以方便地集成到任何网页表单中
- 也方便加入会话控制、访问限制、防机器人保护等功能
现在我们已经掌握了理论基础并选好了开发工具,是时候开始实际操作啦!
我们想制作一个基础版本的验证码,未来可以逐步增加复杂度和更多功能。
- 创建一个任意名称的文件夹(例如 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>
验证码显示:页面加载时,JavaScript 会将验证码图片加载到 <img id="captchaImage"> 元素中。
验证码刷新:点击“刷新”按钮会调用一个函数,通过 JavaScript 更新验证码图片。
验证码验证:用户在输入框输入文字后,点击“验证”按钮,输入的文字会与验证码图片上的文字进行比对。如果输入正确,显示成功信息;如果错误,显示错误信息。
输入等待:只有当用户在输入框输入内容后,“验证”按钮才会被激活。
验证码的所有逻辑都在引入的 JavaScript 文件(script.js)中实现:
2. 创建一个名为 script.js 的新文件 —— 这里将负责加载验证码并将其发送到服务器(目前是本地服务器)进行验证。页面加载时验证码会自动加载。
- 用户输入验证码上的文字。
- 点击“验证”按钮时,会向服务器发送请求以验证输入的文字。
- 验证成功时,显示输入正确的消息,按钮变为不可用。
- 验证失败时,显示错误消息,并加载新的验证码。
- 用户也可以点击“刷新”按钮手动更新验证码。
声明一个变量 captchaId,用于存储验证码的唯一标识符。该标识符用于在服务器端验证用户输入的文本是否对应特定验证码:
let captchaId = "";
3. 创建 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));
}
这里我们向服务器地址 http://localhost:3000/generate-captcha 发送 GET 请求以获取新的验证码图片。
.then((response) => response.json()):将服务器响应转换为 JSON 格式。
.then((data) => {...}):处理获取的数据:
- captchaId = data.captchaId:将服务器返回的验证码 ID 赋值给变量 captchaId,用于后续验证输入的正确性。
- document.getElementById("captchaImage").src = data.captchaImage:将验证码图片的 src 属性设置为服务器返回的图片链接(例如图片 URL)。
.catch((error) => console.error("验证码加载错误:", error)):如果请求或处理响应时发生错误,会在控制台输出错误信息。
4. 为“验证”按钮添加事件监听器:
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:之前获取的验证码ID。
- .then((response) => response.json()):处理服务器响应(验证码校验)。
- .then((result) => {...}):如果验证码校验成功(result.success):
- 显示验证码通过的消息。
- “验证”按钮变为不可用(submitButton.disabled = true)。
出错时:
- 显示错误消息。
- 调用 fetchCaptcha() 函数加载新的验证码图片。
5. 现在添加验证码刷新按钮的事件监听器:
document
.getElementById("refreshButton")
.addEventListener("click", fetchCaptcha);
点击该按钮时,会调用 fetchCaptcha() 函数,加载新的验证码图片。
6. 输入框的事件监听器:
document.getElementById("captchaInput").addEventListener("input", (e) => {
document.getElementById("submitButton").disabled =
e.target.value.trim().length === 0;
});
fetchCaptcha();
每当输入框中的文本发生变化(input 事件)时,都会检查输入内容的长度:
如果文本为空,则“验证”按钮变为不可用(submitButton.disabled = true)。
如果文本不为空,则按钮变为可用(submitButton.disabled = false)。
7. 太好了,验证码的样式和前端验证代码已经完成!现在我们来实现后端部分,也就是生成验证码和验证用户输入的地方。创建一个名为 server.js 的文件,打开终端并安装以下依赖项:
npm install express cors uuid canvas
我们将使用以下库:
- express – Node.js 的一个框架,用于简化 Web 服务器的创建。
- cors – 一个中间件,用于允许跨域请求(让前端可以访问后端接口)。
- uuid – 用于生成唯一的标识符(用于生成 captchaId)。
- canvas – 一个图形库,用于生成验证码图片。
8. 导入安装好的依赖项,创建服务器实例,并添加中间件:
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. 创建一个用于存储验证码的存储对象。我们声明一个名为 captchaStore 的对象,用于保存验证码的相关信息,例如标识符和对应的文本内容:
const captchaStore = {};
10. 生成验证码文本的函数。generateCaptchaText 函数会生成一个由 6 个字符(字母和数字)组成的随机字符串:
function generateCaptchaText() {
return Math.random().toString(36).substring(2, 8).toUpperCase();
}
11. 我们来加点乐趣 —— 比如添加一个随机颜色吧 😀
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})`;
}
12. 接着我们生成一个随机字体:
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}`;
}
13. 现在我们为验证码添加文字、噪点和一些扭曲效果:
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:
14. 验证码生成 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 });
});
15. 验证码验证 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 是否存在于验证码存储中;
- 如果存在,就比较文本内容,并返回结果(正确或错误);
16. 最后一步 —— 启动我们的服务器,监听 3000 端口
app.listen(3000, () => console.log("服务器已启动,监听端口 3000"));
在终端中运行 node server.js,然后在浏览器中打开你的项目页面。太棒了,验证码功能完成并成功运行!

还有一种验证码形式叫作滑块验证码 —— 也就是需要将滑块拖动到表单中的某个位置,比如向右拖动。那我们就自己来动手创建一个吧!
- 我们先创建一个名为 SimpleSlider 的新文件夹,在代码编辑器中新建一个 index.html 文件,写入页面的基本结构,包括验证码容器、滑块和按钮。我们还会为滑块、按钮、成功或错误信息添加样式,并通过内嵌 CSS 实现页面居中排版和美观布局:
<!DOCTYPE html>
<html lang="zh-CN">
<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>
2. 现在我们创建 JavaScript 文件 script.js,用于处理滑块的功能、拖动和与服务器的验证。我们先创建一个变量 captchaSession,用于存储从服务器获取的当前验证码会话 ID,以便之后用于向服务器验证:
let captchaSession = "";
3. 从服务器加载验证码会话:
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
- 期待服务器返回带有 sessionId 字段的 JSON
- 将 sessionId 保存在变量 captchaSession 中
- 如果发生错误,则在控制台输出信息
这样可以在之后验证时将 sessionId 发送到 /verify-slider。
4. 接下来获取所需的 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:如果验证码已完成,防止重复操作
5. 处理滑块鼠标按下事件:
button.addEventListener("mousedown", () => {
if (sliderCompleted) return;
isDragging = true;
});
当按下滑块的鼠标按钮时,会检查用户是否已完成验证码。如果没有,则启用拖动模式(isDragging = true)。
6. 处理滑块移动:
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";
});
当鼠标移动时,我们会检查是否启用了拖动模式,计算鼠标相对于滑块的坐标,并将值限制在滑块边界内。然后设置按钮(滑块)的 left 属性,使其移动。
7. 处理滑块释放事件:
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) {
8. 成功完成滑块后发送到服务器:
fetch("http://localhost:3000/verify-slider", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ sessionId: captchaSession, completed: true }),
})
我们向 /verify-slider 发送一个 POST 请求。请求体中包含:
sessionId – 会话 ID。
completed: true – 用户已将滑块移动到末端。
9. 处理响应:
如果服务器返回 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();
}
})
10. 添加错误处理:重置滑块,加载新验证码。
.catch((error) => {
console.error("验证码验证出错:", error);
errorMessage.style.display = "block";
button.style.left = "0px";
fetchCaptchaSession();
});
11. 如果滑块未拖到末端:重置位置。
} else {
button.style.left = "0px";
}
});
fetchCaptchaSession();
12. 进入服务器部分。在同一目录下创建文件 server.js,打开终端并安装所需依赖:
npm install express cors body-parser
- express 用于搭建 HTTP 服务器。
cors 允许跨域请求。
- body-parser 用于解析 POST 请求中的 JSON 数据。
13. 在项目中引入这些依赖:
import express from "express";
import cors from "cors";
import bodyParser from "body-parser";
14. 配置服务器。创建 Express 应用实例并设置端口(例如 3000):
const app = express();
const PORT = 3000;
15. 允许跨域请求。设置 Express 处理 JSON 请求体。创建用于存储验证码会话的 Map(临时使用,将来推荐用 Redis)。SESSION_TTL 表示会话的有效期:5 分钟。
const sliderSessions = new Map();
const SESSION_TTL = 5 * 60 * 1000;
16. 清理过期会话。每分钟检查一次所有会话,超过 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); // 每分钟执行一次
17. 创建新验证码。生成 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 });
});
18. 验证验证码并启动服务器。从请求体中获取 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}`);
});
19. 在终端中输入命令 node server.js 启动服务器,并在浏览器中打开整个项目。这样就实现了一个简单但功能完整的滑块验证码!

如果你把所有内容都存储在内存中,虽然速度很快,但并不可靠——一旦服务器崩溃,所有数据都会丢失。因此,更好的做法是使用 Redis 来缓存数据。Redis 快速、支持 TTL(生存时间),非常适合存储像 CAPTCHA 和令牌这样的临时数据。
一般的流程如下:你生成一个 CAPTCHA → 将其与一个 ID 一起保存在 Redis 中 → 通过 Cookie 或查询参数将该 ID 发送给用户 → 用户完成 CAPTCHA 验证 → 你验证结果 → 然后决定是否签发令牌。令牌同样存储在 Redis 中,方便之后在登录或提交表单时验证。
Cookie 可用于记录“该用户已经通过了 CAPTCHA 验证”。你可以将 Cookie 设置为持续 10 分钟或 1 小时——根据你的需求决定。时间到期后,将再次显示 CAPTCHA。
你还可以添加计时器以防止垃圾请求,限制 IP 请求频率,以及其他实用的自动化措施——这些 Redis 都能很好地处理。
Redis 非常适合临时存储,但有时你需要长期统计数据:用户完成 CAPTCHA 的次数、来源、失败频率、使用了哪种 CAPTCHA 类型等等。这时就需要使用数据库——比如 PostgreSQL、MongoDB,甚至 MySQL。
数据库对于日志记录、数据分析和可疑行为监控至关重要。
附加参数
除了 captchaId,CAPTCHA 通常还会附带其他参数,例如:

如何处理这些数据
在生成 CAPTCHA 时,将这些字段添加到 CAPTCHA 对象中,并同时存储在 Redis 和数据库中。在 CAPTCHA 验证过程中,更新其状态(成功/失败)并增加尝试次数。如果验证成功,你甚至可以在 Redis 中存储一个授权令牌,并将其与 captchaId 或 IP 地址相关联。如果设置得当,你将不仅拥有一个防护机制,还能构建一个完整的反机器人控制系统 —— 还可以选择添加一个统计面板,用于显示谁在何时、何地、如何通过了哪个 CAPTCHA。
为了加强 CAPTCHA 的安全性,你还可以添加以下功能:
- 噪声、扭曲和随机参数(就像我们在文字 CAPTCHA 中所做的那样)。
- 鼠标移动分析(用于滑块或拼图类型)。
- 混淆和加密处理。
不要将坐标或哈希直接发送到客户端,而是对数据进行签名(例如使用 JWT)。
现在让我们从理论走向实践!想象一个项目,其中用户登录不仅需要输入用户名和密码,还必须通过一个拼图形式的 CAPTCHA。在这个 CAPTCHA 中,用户需要将一块拼图(我们将在项目中从图片中剪切出)拖动到背景图中的正确位置。服务器会验证 CAPTCHA 令牌,只有当 CAPTCHA 验证通过并且登录凭证正确时,用户才能成功登录。
让我们开始动手吧!
首先,准备好所有必要的工具和文件。创建一个名为 Puzzleslider 的文件夹,并在你的代码编辑器中打开它。然后,在一个名为 images 的文件夹中添加一张大小为 320x180 像素的 CAPTCHA 背景图片,作为我们拼图的基础图像。
因为我们要使用 Redis 管理会话和令牌,请在你的电脑上安装 Redis 并启动 Redis 服务器(你可以在官方网站上找到所有必要的安装说明)。
在 Windows 上运行 redis-server:

3. 现在,在 Puzzleslider 文件夹中创建一个名为 index.html 的文件,并使用内联 CSS 添加页面的样式和结构标记:
<!DOCTYPE html>
<html lang="zh">
<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>
这段代码创建了一个页面,包含两个主要部分:验证码表单和登录表单。第一个部分是验证码区域,由两个 <canvas> 组成:一个显示完整背景图像,一个显示拼图碎片。此外还有一个滑块用于验证。第二个部分是登录表单,包括用户名、密码输入框和“登录”按钮。每个部分通过 HTML 元素组织,交互逻辑将通过外部的 script.js 文件中的 JavaScript 实现。
前端的整个过程包括三个主要部分:
- CAPTCHA 生成和验证
- 滑块移动和位置检查
- 在 CAPTCHA 验证成功后提交登录表单
4. 我们将实现拼图 CAPTCHA 和安全登录表单的功能。为此,我们将创建一个名为 script.js 的新文件,并初始化以下变量:
let sessionId = "";
let cutoutX = 0;
let cutoutY = 0;
let isDragging = false;
const puzzleWidth = 320;
const puzzleHeight = 180;
const pieceSize = 50;
- sessionId:存储用于在服务器上验证 CAPTCHA 的唯一会话标识符。
- cutoutX, cutoutY:将被切出并变成拼图块的图像部分的坐标。
- isDragging:一个标志,用于跟踪滑块是否正在被拖动。
- puzzleWidth, puzzleHeight, pieceSize:定义 CAPTCHA 图像和拼图块的尺寸。
5. 接下来,我们将获取 JavaScript 将与之交互的 HTML 元素。
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 是用户将拖动以解决 CAPTCHA 的滑块元素。
- captchaStatus 是一个用于显示 CAPTCHA 状态消息的元素。
- loginButton 是用于提交登录表单的按钮。
6. 现在,我们来创建一个函数以请求新的 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("验证码错误:", err));
}
- fetchCaptcha() 向本地服务器(http://localhost:3000/generate-puzzle-captcha)发送请求,以获取生成新 CAPTCHA 所需的数据。
- 服务器返回以下数据:sessionId(会话标识符)、cutoutX 和 cutoutY(用于切割图像块的坐标)、以及 backgroundImage(背景图像的 URL)。
- 加载图像后,一旦准备好,就调用 drawCaptcha 函数在画布上渲染 CAPTCHA。
- 调用 resetState() 重置界面状态。
7. 绘制 CAPTCHA 并切割拼图块:
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)。
- 然后,清除 CAPTCHA 背景上切割区域(位于 cutoutX 和 cutoutY)。
8. 重置 CAPTCHA 状态 —— 滑块和拼图块返回到初始位置。登录按钮在 CAPTCHA 成功完成之前保持禁用:
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)时,检查滑块的偏移量,并向服务器发送请求以验证 CAPTCHA 是否被正确解决。
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();
- 当用户点击登录按钮时,向服务器发送包含登录数据(用户名、密码和 CAPTCHA 令牌)的请求。
- 如果登录成功,将显示成功消息;否则,显示错误消息。
11. CAPTCHA 样式和所有必要的客户端代码已经准备好——现在我们可以转向服务器部分。这包括存储会话和令牌数据、生成 CAPTCHA、验证 CAPTCHA,以及处理用户认证,并通过令牌进行强制性的 CAPTCHA 验证。
创建一个名为 server.js 的新文件,打开终端,并安装必要的工具:
npm install express cors body-parser @redis/client uuid
- express:用于构建服务器的框架。
- cors:用于配置 CORS(跨源资源共享)的模块,允许服务器接受来自其他域的请求。
- body-parser:用于解析请求体中的 JSON。
- @redis/client:与 Redis 数据库交互的 Redis 库(我们之前已安装并通过 redis-server 启动)。
- uuid:用于生成唯一的会话 ID 和令牌。
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 设置服务器监听 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 错误:", err));
redisClient.connect();
14. 生成和验证 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 });
}
});
这里创建了一个 POST /verify-puzzle 路由,用于验证 CAPTCHA 解决方案。
请求体中发送 sessionId 和 position(滑块位置)。
const session = await redisClient.get(sessionId); 从 Redis 中检索会话数据。如果会话不存在,则返回 400 错误。
const { cutoutX } = JSON.parse(session); 从会话中提取切割坐标。
使用以下代码比较提交的位置与正确位置:
Math.abs(position - cutoutX) <= 10
如果差值在 10 像素以内,则认为 CAPTCHA 通过。
如果 CAPTCHA 通过,则生成一个令牌并将其保存在 Redis 中,以便用于后续操作(令牌有效期为 300 秒)。
然后向客户端发送包含令牌的响应。
15. 带 CAPTCHA 验证的认证:
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 中存在。
如果未找到令牌,说明 CAPTCHA 未通过,将返回 403 错误。
如果 CAPTCHA 验证通过,服务器将继续检查用户名和密码。假设有效凭证为用户名 test 和密码 password,定义在:
if (username === ...)
如果提供的凭证与预定义的凭证匹配,则返回成功响应。
如果凭证不正确(例如,用户名或密码错误),则返回 401 错误。
要查看 Redis 中存储的所有会话和键,可以使用命令 KEYS *。
例如:

让我们检查 Redis 服务器是否正在运行并正常工作。使用命令 node server.js 运行 server.js,并在浏览器中打开项目。尝试测试登录表单和验证码——如果一切都正确完成,解决验证码并输入正确的数据将显示以下视图:

在解决验证码并输入正确的用户名和密码之前,我们无法登录:

我们的代码运行得很好!但我们可以更进一步,创建一个用户无需每次都解决验证码的解决方案。这可以通过在用户浏览器中使用 cookie 来实现。我们建议你考虑以下示例,并尝试开发一个功能齐全的验证码系统,包括令牌和会话存储以及 cookie 支持:
让我们编写代码,创建一个图像验证码系统,用户需要选择属于特定类别的图片,例如汽车、动物或自然。用户访问页面时,将收到一组图片,并需要选择符合类别的图片。服务器将通过随机选择图片并生成唯一的验证码来生成验证码。用户选择图片并点击按钮后,结果将发送到服务器,以检查是否选择了正确的图片。为了避免用户在已经通过验证码后再次解决验证码,数据存储在 Redis 中,并使用 cookie 来跟踪用户是否已经通过验证码。是的,这种方法比之前的示例复杂一些,但结果是值得的!让我们从准备工作开始:
1. 首先,创建一个任意名称的文件夹(在我们的示例中为 Gridcaptcha)。在其中创建一个 images 文件夹,并添加几张尺寸为 150x150 或 100x100 像素的图片。至少从 9 张图片开始——图片越多,验证码的多样性和挑战性就越大!图片应按类别分组,例如:车辆、动物和自然。相应地命名它们,例如 car.jpg、cat.jpg、mountain.jpg 等。
2. 按照之前的示例安装 Redis。
3. 同时,立即创建一个 server.js 文件,并添加所有必要的依赖项:
npm install express cors body-parser morgan @redis/client uuid
- express — 用于创建服务器和路由(例如,处理 /get-captcha 和 /verify-captcha 请求)。
- cors — 中间件,用于启用跨源请求。
- body-parser — 中间件,用于解析 /verify-captcha 请求中发送的数据。
- morgan — 中间件,使用 dev 日志级别,显示简短的请求信息。
- @redis/client — Redis 客户端,用于存储验证码数据并验证用户是否通过了验证码。
- uuid — 用于为验证码和用户生成唯一 ID,以跟踪其会话及相关数据的库。
4. 创建一个 index.html 文件,并添加未来验证码的结构和完整外壳:
<!DOCTYPE html>
<html lang="en">
<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 像素。当选择图片时,图片会放大并以蓝色边框高亮显示。“验证”按钮初始为禁用状态,但在选择图片后会变为激活状态,显示为蓝色边框和白色文本。验证成功或失败的消息将分别以绿色或红色显示。
5. 在新的 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 中的用户标识符。也可以使用 cookie,但在此情况下没有必要,因为 userId 仅在客户端使用,且不会在每次服务器请求中发送(与认证过程不同)。如果没有 userId,则使用 crypto.randomUUID() 生成一个新的并保存到 localStorage 中。
6. 接下来,我们从服务器加载验证码:
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 函数显示可选图片。
7. 渲染图片:
function renderImages(images) {
const container = document.getElementById("captchaImages").textContent;
container.innerHTML = "";
selectedImages.clear();
document.getElementById("submitButton").disabled = true;
images.forEach((imgData, index) => {
const img = document.createElement("img");
img.src = imgData.src;
img.dataset.src = images;
img.addEventListener("click", () => toggleImage(imgData.src, img));
container.appendChild(imgData);
}
}
function toggleImageSelection(img, src) {
if (selectedImages.contains(src)) {
selectedImages.delete(src);
img.classList.remove("selected");
} else {
img.classList.add("selected");
selectedImages.append(src);
}
document.getElementById("submitButton").disabled = selectedImages.size === 0;
}
- renderImages 函数接收一个图片数组,并在 ID 为 captchaImages 的容器内显示它们。
- 为每张图片创建一个 <img> 元素,分配 src 和 data-index 属性,并添加点击事件处理程序。
- 当用户选择图片时,图片会被添加到 selectedImages 集合中。如果没有选择任何图片,提交按钮(submitButton)将保持禁用状态。
- 当用户点击图片时,图片会从或被添加到 selectedImages 集合中。
- 视觉上,这是通过切换 selected 类来反映的,该类会放大图片并将其边框变为蓝色。
- 只有当至少选择了一张图片时,提交按钮才会启用。
8. 向服务器发送选中的图片:
document.getElementById("submitButton").addEventListener("click", async () => {
try {
const response = await fetch("http://localhost:3000/verify-captcha", async () => {
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, .5 seconds); // 1.5秒后刷新验证码
} catch (error) {
console.error("提交验证码错误:", error);
alert("服务器错误,请稍后重试。");
}
});
- 当用户点击“验证”按钮时,向 /verify-captcha 端点发送 POST 请求,传递 captchaId 和选中的图片数组。
- 根据服务器的 的响应(包含 success` 字段的 JSON 对象),显示成功或错误消息。
- 在验证码检查后,setTimeout 函数在 1.5 秒后调用 fetchCaptcha 以刷新验证码。
9. 让我们转到之前创建的 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";
10. 连接到 Redis:
const app = express();
const PORT = 3000;
const redisClient = createClient();
redisClient.connect().catch((err) => console.error("Redis 错误:", err));
- 这里:
- 创建了一个 Express 应用程序实例(app)。
- 设置了服务器运行的端口(3000)。
- 创建了一个 Redis 客户端并建立了连接。
11. 中间件设置:
app.use(cors({ origin: true, credentials: true }));
app.use(bodyParser.json());
app.use(morgan("dev"));
- cors:允许来自任何来源的访问,并支持发送 cookie。
- bodyParser:解析 JSON 请求体。
- morgan:使用“dev”格式将请求记录到控制台。
12. 从 images 文件夹提供静态图片,URL 路径为 /images:
app.use("/images", express.static("images"));
13. 定义图片数组:
每个对象包含图片路径及其类别——未来可以扩展元数据,添加图片或从数据库中获取。
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. 从数组中随机选择指定数量的图片:
function getRandomItems(arr, count) {
return [...arr].sort(() => 0.5 - Math.random()).slice(0, count);
}
15. 添加获取验证码的路由处理程序:
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("获取验证码错误:", err);
res.status(500).json({ success: false, message: "服务器错误" });
}
});
这段代码的功能:
- 检查请求头中是否存在 userId。如果缺失,返回错误。
- 验证用户是否已经通过验证码。如果已通过,发送相应消息。
- 随机选择一个类别(车辆、动物或自然)。
- 生成九张随机图片,并从中识别出符合所选类别的正确图片。
- 将正确图片的信息保存到 Redis 中,设置 5 分钟(300 秒)的过期时间。
- 返回包含图片、所选类别和唯一验证码 ID 的对象。
16. 接下来,添加验证码验证的路由处理程序:
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("验证验证码错误:", err);
res.status(500).json({ success: false, message: "服务器错误" });
}
});
app.listen(PORT, () => {
console.log(`服务器运行在 http://localhost:${PORT}`);
});
- 这里我们从请求体中获取 userId、captchaId 和选中的图片。
- 检查验证码是否存在且未过期。
- 比较选中的图片与正确图片。
- 如果验证码正确解决,将成功状态保存到 Redis 中,设置 1 小时(3600 秒)的过期时间。
- 最后,发送验证结果:成功通过或错误。
17. 最后,启动 redis-server,并使用命令 node server.js 运行 server.js,然后在浏览器中打开项目,访问 http://localhost:3000。是时候测试了!

我们已经创建了一个基于特定类别进行图片选择的功能齐全的验证码!
在成功解决验证码并刷新页面后,我们将看到以下消息,而不是验证码:

我们的“网站”通过记住 userId 并在内存中存储这些数据 1 小时来保持验证码状态。此后,userId 将被重置,用户需要再次解决验证码。
创建的验证码不应保持静态——还有很多增长和增加复杂性的空间。以下是一些改进和扩展系统的想法:
- 添加更多图片!这将增加验证码的复杂性和多样性。
- 随机打乱图片坐标(不仅是 src)。为防止通过图片 URL 进行暴力攻击,可以以 base64 格式返回图片,或使用哈希代替直接的 src 链接。
- 验证 IP 地址 / 用户代理 / 指纹:为防止使用不同 userId 的自动化验证码尝试提供基本保护。
- 在尝试之间设置超时,以减缓重复猜测。
- 颁发一次性 captchaToken 而不是 captcha_${userId},可用于登录或其他安全操作。
- 为 captchaId 添加签名(HMAC)以防止篡改。
- 添加“陷阱”图片——视觉上相似但错误的选项,以增加挑战性。
- 使用 Docker 容器化以实现更稳定的部署。
- 仅使用 HTTPS + SameSite cookie 保护令牌——如果后续集成认证,这一点尤为重要。
- 记录可疑活动,例如重复错误或异常快速的响应。
- 支持语言切换(例如,俄语/英语)以提高可访问性。
- 包含额外的脚本或隐藏元素,进一步混淆验证码行为以防机器人识别。
可以添加的额外 Cookie:

创建自己的验证码远不止是保护一个表单!这是一个集成了布局设计、客户端逻辑、服务器端开发和数据存储的练习。它涵盖了从用户界面到架构决策的方方面面。最终结果是一个紧凑但内容丰富的项目,对于希望同时提升前端和后端技能的人来说尤其有价值。当然,这也是为你的作品集添加独特且实用内容的绝佳方式。
我们希望本指南对你有所帮助——祝你学习顺利,项目成功!
注意:我们想提醒您,该产品用于在您自己的网站上以及您有合法访问权限的网站上进行自动化测试。