@aardpro/captcha-node
v1.0.0
Published
点击式行为验证码后端库 - Node.js implementation
Maintainers
Readme
@aardpro/captcha-node
点击式行为验证码后端库 - Node.js 实现
功能特点
- 🎯 点击式验证:用户按顺序点击图片上的字符
- 🔒 安全加密:使用 AES-256-CBC 加密坐标数据
- ⏰ 过期机制:支持自定义验证码过期时间
- 🎨 随机干扰:内置5张精美背景图,字符随机旋转、缩放、变色
- 🚀 零配置跨平台:使用 SVG + Sharp,无需原生依赖
- 📦 开箱即用:合理的默认配置,减少配置负担
- ✨ TypeScript:完整的类型定义
安装
使用你喜欢的包管理器安装:
# npm
npm install @aardpro/captcha-node
# yarn
yarn add @aardpro/captcha-node
# pnpm
pnpm add @aardpro/captcha-node
# bun
bun add @aardpro/captcha-node系统要求
- Node.js >= 18.0.0
- 本库使用
sharp进行图片处理(预编译二进制,无需额外依赖) - 支持 Windows、Linux、macOS
快速开始
1. 生成验证码
import { generateCaptcha } from '@aardpro/captcha-node';
const { image, token } = await generateCaptcha({
chars: 'ABCDEFGHJKMNPQRSTUVWXYZ23456789',
count: 4,
secret: 'your-secret-key-at-least-20-characters-long'
});
// image: data:image/png;base64,...
// token: 加密后的token字符串返回结果:
image: Base64 格式的 PNG 图片,可直接在前端显示token: 加密的 token,包含坐标和过期时间
2. 前端集成
前端需要:
- 显示
image图片 - 让用户按顺序(从左到右)点击图片上的字符
- 收集点击坐标
[[x1, y1], [x2, y2], ...] - 将坐标和
token发送到后端验证
3. 后端验证
import { verifyCaptcha } from '@aardpro/captcha-node';
const result = verifyCaptcha({
token: '从前端获取的token',
input: [[100, 150], [200, 250]], // 用户点击的坐标数组
secret: 'your-secret-key-at-least-20-characters-long'
});
// result: true (验证成功) 或 false (验证失败)API 文档
generateCaptcha
生成验证码图片和加密 token。
function generateCaptcha(options: GenerateOptions): Promise<GenerateResult>参数
| 参数 | 类型 | 必填 | 默认值 | 说明 |
|------|------|------|--------|------|
| chars | string | ✅ | - | 字符池,可包含任意字符(包括中文),长度不能少于 count |
| count | number | ✅ | - | 显示在图片上的字符数量,范围 2-6 |
| secret | string | ✅ | - | 加密密钥,长度 20-256 字符 |
| exp | number | ❌ | 300 | 过期时间(秒),默认 5 分钟 |
| tolerance | number | ❌ | 10 | 容错半径(像素),用户点击允许的误差范围 |
| width | number | ❌ | 400 | 图片宽度(像素) |
| height | number | ❌ | 300 | 图片高度(像素) |
| margin | number | ❌ | 30 | 字符距离边缘的最小距离(像素) |
返回值
interface GenerateResult {
image: string; // Base64 PNG图片,格式: data:image/png;base64,...
token: string; // 加密token
}示例
// 基本用法
const { image, token } = await generateCaptcha({
chars: 'ABCDEFGHJKMNPQRSTUVWXYZ23456789',
count: 4,
secret: 'my-secret-key-at-least-20-chars'
});
// 自定义参数
const { image, token } = await generateCaptcha({
chars: '验证码测试字符',
count: 6,
secret: 'my-secret-key-at-least-20-chars',
exp: 600, // 10分钟过期
tolerance: 15, // 容错半径15像素
width: 500,
height: 400
});verifyCaptcha
验证用户点击的坐标是否正确。
function verifyCaptcha(options: VerifyOptions): boolean参数
| 参数 | 类型 | 必填 | 说明 |
|------|------|------|------|
| token | string | ✅ | 从 generateCaptcha 获取的 token |
| input | Array<[number, number]> | ✅ | 用户点击的坐标数组,必须按字符出现顺序(从左到右) |
| secret | string | ✅ | 与生成时使用的相同密钥 |
返回值
true: 验证成功false: 验证失败(坐标错误、过期或 token 无效)
示例
const isValid = verifyCaptcha({
token: '从generateCaptcha获取的token',
input: [
[100, 150], // 第一个字符的点击位置
[200, 250], // 第二个字符的点击位置
],
secret: 'my-secret-key-at-least-20-chars'
});
if (isValid) {
console.log('验证通过!');
} else {
console.log('验证失败:坐标错误、超时或token无效');
}架构设计
无状态设计(Stateless)
本库采用无状态设计,所有需要验证的信息都加密在 token 中:
生成验证码:后端生成 { image, token } → 都返回给前端
前端存储:前端保存 token(无法解密,无法伪造)
用户点击:前端收集坐标 input
验证请求:前端发送 { input, token } → 后端验证
后端验证:解密 token → 验证坐标 → 返回结果安全性分析
为什么这样设计是安全的?
前端无法伪造 token
- token 使用 AES-256-CBC 加密
- 没有 secret 密钥,前端无法生成有效 token
前端无法获取真实坐标
- 坐标信息加密存储在 token 中
- 前端只能看到加密字符串,无法解密
token 可以公开传输
- 即使 token 被拦截,没有 secret 也无法解密
- token 包含过期时间,自动失效
优势
- ✅ 无需 session/Redis:服务器不需要存储状态
- ✅ 水平扩展友好:多个服务器实例都可以验证
- ✅ 简单部署:不需要额外的存储服务
- ✅ API 简洁:只需两个接口(生成 + 验证)
权衡
- ⚠️ token 可以多次使用(在过期时间内)
- 💡 如需防止重放,可在应用层添加已使用token的记录(见安全建议)
完整示例
Express 集成示例
import express from 'express';
import { generateCaptcha, verifyCaptcha } from '@aardpro/captcha-node';
const app = express();
app.use(express.json());
const SECRET = 'your-app-secret-key-at-least-20-characters-long';
// 生成验证码(无状态设计)
app.get('/api/captcha', async (req, res) => {
try {
const { image, token } = await generateCaptcha({
chars: 'ABCDEFGHJKMNPQRSTUVWXYZ23456789',
count: 4,
secret: SECRET
});
// 将 image 和 token 都返回给前端
// token 是加密的,前端无法解密,也无法伪造
res.json({ image, token });
} catch (error) {
res.status(500).json({ error: '生成验证码失败' });
}
});
// 验证验证码(无状态设计)
app.post('/api/verify', (req, res) => {
try {
const { input, token } = req.body;
if (!input || !token) {
return res.status(400).json({ error: '缺少必要参数' });
}
const isValid = verifyCaptcha({
token, // 从前端接收的加密token
input, // 用户点击的坐标
secret: SECRET
});
res.json({ valid: isValid });
} catch (error) {
res.status(500).json({ error: '验证失败' });
}
});
app.listen(3000);前端示例(HTML + JavaScript)
<!DOCTYPE html>
<html>
<head>
<title>验证码示例</title>
</head>
<body>
<div id="captcha-container">
<img id="captcha-image" src="" alt="验证码" style="cursor: pointer;" />
<p>请按顺序点击图片上的字符(从左到右)</p>
<button onclick="submitCaptcha()">提交</button>
<button onclick="refreshCaptcha()">刷新</button>
</div>
<script>
let clicks = [];
let currentToken = '';
// 加载验证码
async function refreshCaptcha() {
clicks = [];
const response = await fetch('/api/captcha');
const data = await response.json();
// 保存token和显示图片
currentToken = data.token;
document.getElementById('captcha-image').src = data.image;
}
// 记录点击位置
document.getElementById('captcha-image').addEventListener('click', (e) => {
const rect = e.target.getBoundingClientRect();
const x = Math.round(e.clientX - rect.left);
const y = Math.round(e.clientY - rect.top);
clicks.push([x, y]);
// 可选:在点击位置显示标记
showClickMarker(e.clientX, e.clientY);
});
// 提交验证
async function submitCaptcha() {
const response = await fetch('/api/verify', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
input: clicks,
token: currentToken // 将token一起发送
})
});
const data = await response.json();
if (data.valid) {
alert('验证通过!');
} else {
alert('验证失败,请重试');
refreshCaptcha();
}
}
// 页面加载时获取验证码
refreshCaptcha();
</script>
</body>
</html>安全建议
密钥管理
secret应该存储在环境变量中,不要硬编码在代码里- 使用足够长的密钥(至少20个字符)
- 定期更换密钥
防重放攻击
- 本库采用无状态设计,token可以多次验证(只要未过期)
- 如需防止重放,建议在应用层实现:
- 使用 Redis 存储已使用的token(验证成功后set,过期时间与token一致)
- 或在token中添加nonce,验证成功后记录到数据库
- token本身有过期时间限制(默认5分钟)
防暴力破解
- 在应用层面限制验证失败次数(如使用 Redis 计数)
- 失败次数过多时临时封禁 IP 或用户
- 不要返回具体的错误原因(如"坐标错误"或"已过期")
过期时间
- 根据业务需求设置合理的过期时间
- 默认 5 分钟适用于大多数场景
- 敏感操作建议使用更短的过期时间
常见问题
Q: 安装失败怎么办?
A: 本库使用预编译的二进制文件,通常不会出现安装问题。如果遇到问题:
- 确保使用的是 Node.js >= 18.0.0
- 尝试清除缓存:
npm cache clean --force - 删除 node_modules 后重新安装
Q: 如何提高验证码的安全性?
A:
- 使用更长的字符池(包含中文、数字、字母混合)
- 增加字符数量(count 参数)
- 缩短过期时间
- 在应用层面添加频率限制
Q: 为什么验证总是失败?
A: 检查以下几点:
- 确保用户按顺序点击(从左到右)
- 确保坐标格式正确
[[x1, y1], [x2, y2], ...] - 确保 secret 字符串完全一致
- 检查 token 是否已过期
Q: 可以自定义背景图吗?
A: 当前版本使用内置的 5 张背景图。如需自定义,可以修改源码中的 src/images/backgrounds.ts 文件。
Q: 支持哪些 Node.js 版本?
A: 需要 Node.js >= 18.0.0
开发
# 克隆仓库
git clone <repository-url>
# 安装依赖
npm install
# 构建
npm run build
# 运行测试
npm test
# 开发模式(监听文件变化)
npm run devLicense
MIT
支持
如有问题或建议,请提交 Issue。
