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; | |||
|  | }; |