220 lines
5.9 KiB
JavaScript
220 lines
5.9 KiB
JavaScript
|
|
const express = require('express');
|
|||
|
|
const crypto = require('crypto');
|
|||
|
|
const router = express.Router();
|
|||
|
|
|
|||
|
|
|
|||
|
|
|
|||
|
|
// 内存存储验证码(生产环境建议使用Redis)
|
|||
|
|
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 生成随机验证码字符串
|
|||
|
|
* @param {number} length 验证码长度
|
|||
|
|
* @returns {string} 验证码字符串
|
|||
|
|
*/
|
|||
|
|
function generateCaptchaText(length = 4) {
|
|||
|
|
const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789';
|
|||
|
|
let result = '';
|
|||
|
|
for (let i = 0; i < length; i++) {
|
|||
|
|
result += chars.charAt(Math.floor(Math.random() * chars.length));
|
|||
|
|
}
|
|||
|
|
return result;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 生成SVG验证码图片
|
|||
|
|
* @param {string} text 验证码文本
|
|||
|
|
* @returns {string} SVG字符串
|
|||
|
|
*/
|
|||
|
|
function generateCaptchaSVG(text) {
|
|||
|
|
const width = 120;
|
|||
|
|
const height = 40;
|
|||
|
|
const fontSize = 18;
|
|||
|
|
|
|||
|
|
// 生成随机颜色
|
|||
|
|
const getRandomColor = () => {
|
|||
|
|
const colors = ['#FF6B6B', '#4ECDC4', '#45B7D1', '#96CEB4', '#FFEAA7', '#DDA0DD', '#98D8C8'];
|
|||
|
|
return colors[Math.floor(Math.random() * colors.length)];
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
// 生成干扰线
|
|||
|
|
const generateNoise = () => {
|
|||
|
|
let noise = '';
|
|||
|
|
for (let i = 0; i < 3; i++) {
|
|||
|
|
const x1 = Math.random() * width;
|
|||
|
|
const y1 = Math.random() * height;
|
|||
|
|
const x2 = Math.random() * width;
|
|||
|
|
const y2 = Math.random() * height;
|
|||
|
|
noise += `<line x1="${x1}" y1="${y1}" x2="${x2}" y2="${y2}" stroke="${getRandomColor()}" stroke-width="1" opacity="0.3"/>`;
|
|||
|
|
}
|
|||
|
|
return noise;
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
// 生成干扰点
|
|||
|
|
const generateDots = () => {
|
|||
|
|
let dots = '';
|
|||
|
|
for (let i = 0; i < 20; i++) {
|
|||
|
|
const x = Math.random() * width;
|
|||
|
|
const y = Math.random() * height;
|
|||
|
|
const r = Math.random() * 2 + 1;
|
|||
|
|
dots += `<circle cx="${x}" cy="${y}" r="${r}" fill="${getRandomColor()}" opacity="0.4"/>`;
|
|||
|
|
}
|
|||
|
|
return dots;
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
// 生成文字
|
|||
|
|
let textElements = '';
|
|||
|
|
const charWidth = width / text.length;
|
|||
|
|
|
|||
|
|
for (let i = 0; i < text.length; i++) {
|
|||
|
|
const char = text[i];
|
|||
|
|
const x = charWidth * i + charWidth / 2;
|
|||
|
|
const y = height / 2 + fontSize / 3;
|
|||
|
|
const rotation = (Math.random() - 0.5) * 30; // 随机旋转角度
|
|||
|
|
const color = getRandomColor();
|
|||
|
|
|
|||
|
|
textElements += `
|
|||
|
|
<text x="${x}" y="${y}"
|
|||
|
|
font-family="Arial, sans-serif"
|
|||
|
|
font-size="${fontSize}"
|
|||
|
|
font-weight="bold"
|
|||
|
|
fill="${color}"
|
|||
|
|
text-anchor="middle"
|
|||
|
|
transform="rotate(${rotation} ${x} ${y})">
|
|||
|
|
${char}
|
|||
|
|
</text>`;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
const svg = `
|
|||
|
|
<svg width="${width}" height="${height}" xmlns="http://www.w3.org/2000/svg">
|
|||
|
|
<defs>
|
|||
|
|
<linearGradient id="bg" x1="0%" y1="0%" x2="100%" y2="100%">
|
|||
|
|
<stop offset="0%" style="stop-color:#f8f9fa;stop-opacity:1" />
|
|||
|
|
<stop offset="100%" style="stop-color:#e9ecef;stop-opacity:1" />
|
|||
|
|
</linearGradient>
|
|||
|
|
</defs>
|
|||
|
|
<rect width="${width}" height="${height}" fill="url(#bg)" stroke="#dee2e6" stroke-width="1"/>
|
|||
|
|
${generateNoise()}
|
|||
|
|
${generateDots()}
|
|||
|
|
${textElements}
|
|||
|
|
</svg>`;
|
|||
|
|
|
|||
|
|
return svg;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
|
|||
|
|
router.get('/generate', (req, res) => {
|
|||
|
|
try {
|
|||
|
|
// 生成验证码文本
|
|||
|
|
const captchaText = generateCaptchaText();
|
|||
|
|
|
|||
|
|
// 生成唯一ID
|
|||
|
|
const captchaId = crypto.randomUUID();
|
|||
|
|
|
|||
|
|
// 存储验证码(5分钟过期)
|
|||
|
|
global.captchaStore.set(captchaId, {
|
|||
|
|
text: captchaText.toLowerCase(), // 存储小写用于比较
|
|||
|
|
expires: Date.now() + 5 * 60 * 1000 // 5分钟过期
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
// 生成SVG图片
|
|||
|
|
const svgImage = generateCaptchaSVG(captchaText);
|
|||
|
|
res.json({
|
|||
|
|
success: true,
|
|||
|
|
data: {
|
|||
|
|
captchaId,
|
|||
|
|
image: `data:image/svg+xml;base64,${Buffer.from(svgImage).toString('base64')}`
|
|||
|
|
}
|
|||
|
|
});
|
|||
|
|
} catch (error) {
|
|||
|
|
console.error('生成验证码失败:', error);
|
|||
|
|
res.status(500).json({
|
|||
|
|
success: false,
|
|||
|
|
message: '生成验证码失败'
|
|||
|
|
});
|
|||
|
|
}
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
|
|||
|
|
router.post('/verify', (req, res) => {
|
|||
|
|
try {
|
|||
|
|
const { captchaId, captchaText } = req.body;
|
|||
|
|
|
|||
|
|
if (!captchaId || !captchaText) {
|
|||
|
|
return res.status(400).json({
|
|||
|
|
success: false,
|
|||
|
|
message: '验证码ID和验证码不能为空'
|
|||
|
|
});
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 获取存储的验证码
|
|||
|
|
const storedCaptcha = global.captchaStore.get(captchaId);
|
|||
|
|
|
|||
|
|
if (!storedCaptcha) {
|
|||
|
|
return res.status(400).json({
|
|||
|
|
success: false,
|
|||
|
|
message: '验证码不存在或已过期'
|
|||
|
|
});
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 检查是否过期
|
|||
|
|
if (Date.now() > storedCaptcha.expires) {
|
|||
|
|
global.captchaStore.delete(captchaId);
|
|||
|
|
return res.status(400).json({
|
|||
|
|
success: false,
|
|||
|
|
message: '验证码已过期'
|
|||
|
|
});
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 验证验证码(不区分大小写)
|
|||
|
|
const isValid = storedCaptcha.text === captchaText.toLowerCase();
|
|||
|
|
|
|||
|
|
// 验证后删除验证码(无论成功失败)
|
|||
|
|
global.captchaStore.delete(captchaId);
|
|||
|
|
|
|||
|
|
if (isValid) {
|
|||
|
|
res.json({
|
|||
|
|
success: true,
|
|||
|
|
message: '验证码验证成功'
|
|||
|
|
});
|
|||
|
|
} else {
|
|||
|
|
res.status(400).json({
|
|||
|
|
success: false,
|
|||
|
|
message: '验证码错误'
|
|||
|
|
});
|
|||
|
|
}
|
|||
|
|
} catch (error) {
|
|||
|
|
console.error('验证验证码失败:', error);
|
|||
|
|
res.status(500).json({
|
|||
|
|
success: false,
|
|||
|
|
message: '验证验证码失败'
|
|||
|
|
});
|
|||
|
|
}
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
// 清理过期验证码的定时任务
|
|||
|
|
setInterval(() => {
|
|||
|
|
const now = Date.now();
|
|||
|
|
for (const [id, captcha] of global.captchaStore.entries()) {
|
|||
|
|
if (now > captcha.expires) {
|
|||
|
|
global.captchaStore.delete(id);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}, 60 * 1000); // 每分钟清理一次
|
|||
|
|
|
|||
|
|
// 导出验证函数供其他模块使用
|
|||
|
|
module.exports = router;
|
|||
|
|
module.exports.verifyCaptcha = (captchaId, captchaText) => {
|
|||
|
|
const captcha = global.captchaStore.get(captchaId);
|
|||
|
|
if (!captcha) {
|
|||
|
|
return false; // 验证码不存在或已过期
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
if (captcha.text.toLowerCase() !== captchaText.toLowerCase()) {
|
|||
|
|
return false; // 验证码错误
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 验证成功后删除验证码(一次性使用)
|
|||
|
|
global.captchaStore.delete(captchaId);
|
|||
|
|
return true;
|
|||
|
|
};
|