UNPKG

click-captcha

Version:

Chinese Character Sequence Click Verification System

140 lines (139 loc) 5.88 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.ClickCaptcha = void 0; const utils_1 = require("../utils"); const svg_builder_1 = require("./svg-builder"); const sharp = require("sharp"); const options_1 = require("../const/options"); class ClickCaptcha { constructor(options) { this.options = (0, utils_1.deepMerge)(options_1.DEFAULT_OPTIONS, options || {}); this.validateOptions(); } validateOptions() { const { characters } = this.options; if (!characters.pool || characters.pool.length < 10) { throw new Error("至少需要提供 10 个字符"); } if (characters.pool.length < characters.count) { throw new Error("字符集长度小于生成的字符数量"); } } /** * 生成随机位置 */ getRandomPosition(existingPositions = []) { const { dimensions, font, characters, security } = this.options; const minDistance = font.fontSize * characters.minSpacing; let attempts = 0; while (attempts < security.positionGenerationAttempts) { const x = dimensions.padding + Math.random() * (dimensions.width - 2 * dimensions.padding); const y = dimensions.padding + Math.random() * (dimensions.height - 2 * dimensions.padding); // 碰撞检测 const isOverlap = existingPositions.some((pos) => { const dx = pos.x - x; const dy = pos.y - y; return Math.sqrt(dx * dx + dy * dy) < minDistance; }); if (!isOverlap) { return { x, y }; } attempts++; } // 超过尝试次数则返回随机位置 return { x: dimensions.padding + Math.random() * (dimensions.width - 2 * dimensions.padding), y: dimensions.padding + Math.random() * (dimensions.height - 2 * dimensions.padding), }; } /** * 生成随机字符 */ getRandomChar(existingChars) { const { characters } = this.options; const allChars = characters.pool.split(""); const filteredChars = allChars.filter((char) => !existingChars.includes(char)); return filteredChars[utils_1.Random.int(0, filteredChars.length)]; } /** * @author Qing * @description 生成点击验证码 * @return {Promise<CaptchaResult>} 验证码结果 * @example * const captcha = new ClickCaptcha(); * const { image, hint, data } = await captcha.generate(); * @date 2025-02-14 10:21:01 */ async generate() { const { characters } = this.options; try { // 生成随机字符和位置 const existingPositions = []; const chars = []; Array.from({ length: characters.count }, () => { const char = this.getRandomChar(chars.map((c) => c.char)); const pos = this.getRandomPosition(existingPositions); existingPositions.push(pos); chars.push({ char, coordinates: pos }); }); // 生成主图片并转换为base64 const mainSvg = svg_builder_1.SvgBuilder.buildMain(chars, this.options); const mainBuffer = await sharp(Buffer.from(mainSvg)).png().toBuffer(); const mainBase64 = `data:image/png;base64,${mainBuffer.toString("base64")}`; // 生成提示图片并转换为base64 const hintSvg = svg_builder_1.SvgBuilder.buildHint(chars, this.options); const hintBuffer = await sharp(Buffer.from(hintSvg)).png().toBuffer(); const hintBase64 = `data:image/png;base64,${hintBuffer.toString("base64")}`; return { imageBase64: mainBase64, hintBase64: hintBase64, verificationPoints: chars, }; } catch (error) { throw new Error(`验证码生成失败: ${error}`); } } /** * @author Qing * @description 验证点击位置 * @param {Coordinate[]} userPositions 用户点击位置 * @param {VerificationPoint[]} verificationPoints 数据中的正确的验证码字符位置 * @return {boolean} 是否验证通过 * @date 2025-02-14 10:20:16 */ verify(userPositions, verificationPoints) { const { security, dimensions } = this.options; const tolerance = security.clickTolerance; if (userPositions.length !== verificationPoints.length) { return false; } console.info(new Date().toLocaleString(), "用户点击位置:", userPositions.map((p) => ({ x: p.x * dimensions.width, y: p.y * dimensions.height, })), "验证码字符位置:", verificationPoints.map((p) => ({ x: p.coordinates.x, y: p.coordinates.y, })), "正确信息:", verificationPoints.map((p) => ({ x: p.coordinates.x / dimensions.width, y: p.coordinates.y / dimensions.height, }))); return userPositions.every((pos, index) => { const char = verificationPoints[index].coordinates; // 将百分比转换为原始坐标系的绝对位置 const clickX = pos.x * dimensions.width; const clickY = pos.y * dimensions.height; const distance = Math.sqrt(Math.pow(clickX - char.x, 2) + Math.pow(clickY - char.y, 2)); const res = distance <= tolerance; if (!res) { console.info(`第${index + 1}个字符点击位置错误,正确位置:${char.x},${char.y},点击位置:${clickX},${clickY},距离:${distance}`); } return res; }); } } exports.ClickCaptcha = ClickCaptcha;