const express = require('express'); const crypto = require('crypto'); const router = express.Router(); // 内存存储验证码(生产环境建议使用Redis) const captchaStore = new Map(); /** * 生成随机验证码字符串 * @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 += ``; } 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 += ``; } 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 += ` ${char} `; } const svg = ` ${generateNoise()} ${generateDots()} ${textElements} `; return svg; } /** * 生成验证码接口 */ router.get('/generate', (req, res) => { try { // 生成验证码文本 const captchaText = generateCaptchaText(); // 生成唯一ID const captchaId = crypto.randomUUID(); // 存储验证码(5分钟过期) 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: '生成验证码失败' }); } }); /** * 验证验证码接口 * @param {string} captchaId 验证码ID * @param {string} captchaText 用户输入的验证码 */ 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 = captchaStore.get(captchaId); if (!storedCaptcha) { return res.status(400).json({ success: false, message: '验证码不存在或已过期' }); } // 检查是否过期 if (Date.now() > storedCaptcha.expires) { captchaStore.delete(captchaId); return res.status(400).json({ success: false, message: '验证码已过期' }); } // 验证验证码(不区分大小写) const isValid = storedCaptcha.text === captchaText.toLowerCase(); // 验证后删除验证码(无论成功失败) 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 captchaStore.entries()) { if (now > captcha.expires) { captchaStore.delete(id); } } }, 60 * 1000); // 每分钟清理一次 // 导出验证函数供其他模块使用 module.exports = router; module.exports.verifyCaptcha = (captchaId, captchaText) => { const captcha = captchaStore.get(captchaId); if (!captcha) { return false; // 验证码不存在或已过期 } if (captcha.text.toLowerCase() !== captchaText.toLowerCase()) { return false; // 验证码错误 } // 验证成功后删除验证码(一次性使用) captchaStore.delete(captchaId); return true; };