{"_id":"@aardpro/captcha-node","_rev":"2-815cc3a3da54c891e13782eb5adbbc39","name":"@aardpro/captcha-node","dist-tags":{"latest":"3.0.0"},"versions":{"1.0.0":{"name":"@aardpro/captcha-node","version":"1.0.0","keywords":["captcha","verification","click-captcha","behavior-verification"],"author":{"name":"AardPro","email":"contact@aardpro.com"},"license":"MIT","_id":"@aardpro/captcha-node@1.0.0","maintainers":[{"name":"aardpro","email":"chileehong@outlook.com"}],"homepage":"https://github.com/aardpro/captcha-node#readme","bugs":{"url":"https://github.com/aardpro/captcha-node/issues"},"dist":{"shasum":"12c45d116f74ac29720d98c82a5041277079f53c","tarball":"https://registry.npmjs.org/@aardpro/captcha-node/-/captcha-node-1.0.0.tgz","fileCount":24,"integrity":"sha512-wcIg+Vw6uPMNZhyVst0PO0pjuoOFY+m+UZ/zQp+Z9DE9GP/Ad5PLVzf7QNOrgmYgge99so2w4R6ZcTArMCJGiA==","signatures":[{"sig":"MEUCIFfXGLHvCqa+Hvz+7RgTand3/gCDmSP2uVnxHFmbByzRAiEAm3scclhf+97/38IKKH8IaKOcyCYhsjBbc3KZ/4XeWqw=","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":658689},"main":"dist/index.js","type":"module","types":"dist/index.d.ts","engines":{"node":">=18.0.0"},"exports":{".":{"types":"./dist/index.d.ts","import":"./dist/index.js"}},"scripts":{"dev":"tsc --watch","test":"node --test","build":"tsc","prepublishOnly":"npm run build"},"_npmUser":{"name":"aardpro","email":"chileehong@outlook.com"},"repository":{"url":"git+https://github.com/aardpro/captcha-node.git","type":"git"},"_npmVersion":"11.6.0","description":"点击式行为验证码后端库 - Node.js implementation","directories":{},"_nodeVersion":"22.19.0","dependencies":{"sharp":"^0.33.0"},"_hasShrinkwrap":false,"devDependencies":{"typescript":"^5.3.0","@types/node":"^20.10.0"},"_npmOperationalInternal":{"tmp":"tmp/captcha-node_1.0.0_1769499535707_0.0070442392993257386","host":"s3://npm-registry-packages-npm-production"}},"3.0.0":{"name":"@aardpro/captcha-node","version":"3.0.0","description":"点击式行为验证码后端库 - Node.js implementation","type":"module","main":"dist/index.js","types":"dist/index.d.ts","exports":{".":{"import":"./dist/index.js","types":"./dist/index.d.ts"}},"scripts":{"build":"tsc","dev":"tsc --watch","test":"node --test","prepublishOnly":"npm run build"},"keywords":["captcha","verification","click-captcha","behavior-verification"],"author":{"name":"AardPro","email":"contact@aardpro.com"},"license":"MIT","repository":{"type":"git","url":"git+https://github.com/aardpro/captcha-node.git"},"bugs":{"url":"https://github.com/aardpro/captcha-node/issues"},"homepage":"https://github.com/aardpro/captcha-node#readme","devDependencies":{"@types/node":"^20.10.0","typescript":"^5.3.0"},"dependencies":{"sharp":"^0.33.0"},"engines":{"node":">=18.0.0"},"gitHead":"a68ed98922f84891d837d156d5726748d20562f7","_id":"@aardpro/captcha-node@3.0.0","_nodeVersion":"22.23.1","_npmVersion":"12.0.2","dist":{"integrity":"sha512-KvhY3TLX3rQ725TJjEi3aWzLe2Odi0FUqrmoj9s0qVAoq6UhuEMGKA5ji700Hy9bjMRP2KOw7o1UUEfKpMra9g==","shasum":"3c6c204e98c5ccb0611ba5ee0c7b053fd8045043","tarball":"https://registry.npmjs.org/@aardpro/captcha-node/-/captcha-node-3.0.0.tgz","fileCount":27,"unpackedSize":662675,"attestations":{"url":"https://registry.npmjs.org/-/npm/v1/attestations/@aardpro%2fcaptcha-node@3.0.0","provenance":{"predicateType":"https://slsa.dev/provenance/v1"}},"signatures":[{"keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U","sig":"MEYCIQC7JspDYSLsDZsT+UppPMoyo2egwvkQxM48BBk5+6MljgIhAJkT9hmUN7ZOAZr0Vvb52gclKaynYcrakw9UgIZ0K5gC"}]},"_npmUser":{"name":"GitHub Actions","email":"npm-oidc-no-reply@github.com","trustedPublisher":{"id":"github","oidcConfigId":"oidc:9c57fa88-1475-4733-84bb-f3d21757a241"}},"directories":{},"maintainers":[{"name":"aardpro","email":"chileehong@outlook.com"}],"_npmOperationalInternal":{"host":"s3://npm-registry-packages-npm-production","tmp":"tmp/captcha-node_3.0.0_1786092228686_0.6418877786098685"},"_hasShrinkwrap":false}},"time":{"created":"2026-01-27T07:38:55.620Z","modified":"2026-08-07T08:43:49.152Z","1.0.0":"2026-01-27T07:38:55.882Z","3.0.0":"2026-08-07T08:43:48.847Z"},"bugs":{"url":"https://github.com/aardpro/captcha-node/issues"},"author":{"name":"AardPro","email":"contact@aardpro.com"},"license":"MIT","homepage":"https://github.com/aardpro/captcha-node#readme","keywords":["captcha","verification","click-captcha","behavior-verification"],"repository":{"type":"git","url":"git+https://github.com/aardpro/captcha-node.git"},"description":"点击式行为验证码后端库 - Node.js implementation","maintainers":[{"name":"aardpro","email":"chileehong@outlook.com"}],"readme":"# @aardpro/captcha-node\n\n点击式行为验证码后端库 - Node.js 实现\n\n## 功能特点\n\n- 🎯 点击式验证：用户按顺序点击图片上的字符\n- 🔒 安全加密：使用 AES-256-CBC 加密坐标数据\n- ⏰ 过期机制：支持自定义验证码过期时间\n- 🎨 随机干扰：内置5张精美背景图，字符随机旋转、缩放、变色\n- 🚀 零配置跨平台：使用 SVG + Sharp，无需原生依赖\n- 📦 开箱即用：合理的默认配置，减少配置负担\n- ✨ TypeScript：完整的类型定义\n\n## 安装\n\n使用你喜欢的包管理器安装：\n\n```bash\n# npm\nnpm install @aardpro/captcha-node\n\n# yarn\nyarn add @aardpro/captcha-node\n\n# pnpm\npnpm add @aardpro/captcha-node\n\n# bun\nbun add @aardpro/captcha-node\n```\n\n### 系统要求\n\n- Node.js >= 18.0.0\n- 本库使用 `sharp` 进行图片处理（预编译二进制，无需额外依赖）\n- 支持 Windows、Linux、macOS\n\n## 快速开始\n\n### 1. 生成验证码\n\n```typescript\nimport { generateCaptcha } from '@aardpro/captcha-node';\n\nconst { image, token } = await generateCaptcha({\n  chars: 'ABCDEFGHJKMNPQRSTUVWXYZ23456789',\n  count: 4,\n  secret: 'your-secret-key-at-least-20-characters-long'\n});\n\n// image: data:image/png;base64,...\n// token: 加密后的token字符串\n```\n\n**返回结果：**\n\n- `image`: Base64 格式的 PNG 图片，可直接在前端显示\n- `token`: 加密的 token，包含坐标和过期时间\n\n### 2. 前端集成\n\n前端需要：\n1. 显示 `image` 图片\n2. 让用户按顺序（从左到右）点击图片上的字符\n3. 收集点击坐标 `[[x1, y1], [x2, y2], ...]`\n4. 将坐标和 `token` 发送到后端验证\n\n### 3. 后端验证\n\n```typescript\nimport { verifyCaptcha } from '@aardpro/captcha-node';\n\nconst result = verifyCaptcha({\n  token: '从前端获取的token',\n  input: [[100, 150], [200, 250]], // 用户点击的坐标数组\n  secret: 'your-secret-key-at-least-20-characters-long'\n});\n\n// result: true (验证成功) 或 false (验证失败)\n```\n\n## API 文档\n\n### generateCaptcha\n\n生成验证码图片和加密 token。\n\n```typescript\nfunction generateCaptcha(options: GenerateOptions): Promise<GenerateResult>\n```\n\n#### 参数\n\n| 参数 | 类型 | 必填 | 默认值 | 说明 |\n|------|------|------|--------|------|\n| `chars` | `string` | ✅ | - | 字符池，可包含任意字符（包括中文），长度不能少于 `count` |\n| `count` | `number` | ✅ | - | 显示在图片上的字符数量，范围 2-6 |\n| `secret` | `string` | ✅ | - | 加密密钥，长度 20-256 字符 |\n| `exp` | `number` | ❌ | `300` | 过期时间（秒），默认 5 分钟 |\n| `tolerance` | `number` | ❌ | `10` | 容错半径（像素），用户点击允许的误差范围 |\n| `width` | `number` | ❌ | `400` | 图片宽度（像素） |\n| `height` | `number` | ❌ | `300` | 图片高度（像素） |\n| `margin` | `number` | ❌ | `30` | 字符距离边缘的最小距离（像素） |\n| `lang` | `'cn' \\| 'en'` | ❌ | `'cn'` | 语言，控制校验错误信息的语言（非法值回退 `cn`） |\n\n#### 返回值\n\n```typescript\ninterface GenerateResult {\n  image: string;  // Base64 PNG图片，格式: data:image/png;base64,...\n  token: string;  // 加密token\n}\n```\n\n#### 示例\n\n```typescript\n// 基本用法\nconst { image, token } = await generateCaptcha({\n  chars: 'ABCDEFGHJKMNPQRSTUVWXYZ23456789',\n  count: 4,\n  secret: 'my-secret-key-at-least-20-chars'\n});\n\n// 自定义参数\nconst { image, token } = await generateCaptcha({\n  chars: '验证码测试字符',\n  count: 6,\n  secret: 'my-secret-key-at-least-20-chars',\n  exp: 600,        // 10分钟过期\n  tolerance: 15,   // 容错半径15像素\n  width: 500,\n  height: 400\n});\n```\n\n### verifyCaptcha\n\n验证用户点击的坐标是否正确。\n\n```typescript\nfunction verifyCaptcha(options: VerifyOptions): boolean\n```\n\n#### 参数\n\n| 参数 | 类型 | 必填 | 说明 |\n|------|------|------|------|\n| `token` | `string` | ✅ | 从 `generateCaptcha` 获取的 token |\n| `input` | `Array<[number, number]>` | ✅ | 用户点击的坐标数组，必须按字符出现顺序（从左到右） |\n| `secret` | `string` | ✅ | 与生成时使用的相同密钥 |\n| `lang` | `'cn' \\| 'en'` | ❌ | 语言，控制校验错误信息的语言（默认 `cn`，非法值回退 `cn`） |\n\n#### 返回值\n\n- `true`: 验证成功\n- `false`: 验证失败（坐标错误、过期或 token 无效）\n\n#### 示例\n\n```typescript\nconst isValid = verifyCaptcha({\n  token: '从generateCaptcha获取的token',\n  input: [\n    [100, 150], // 第一个字符的点击位置\n    [200, 250], // 第二个字符的点击位置\n  ],\n  secret: 'my-secret-key-at-least-20-chars'\n});\n\nif (isValid) {\n  console.log('验证通过！');\n} else {\n  console.log('验证失败：坐标错误、超时或token无效');\n}\n```\n\n## 架构设计\n\n### 无状态设计（Stateless）\n\n本库采用**无状态设计**，所有需要验证的信息都加密在 token 中：\n\n```\n生成验证码：后端生成 { image, token } → 都返回给前端\n前端存储：前端保存 token（无法解密，无法伪造）\n用户点击：前端收集坐标 input\n验证请求：前端发送 { input, token } → 后端验证\n后端验证：解密 token → 验证坐标 → 返回结果\n```\n\n### 安全性分析\n\n**为什么这样设计是安全的？**\n\n1. **前端无法伪造 token**\n   - token 使用 AES-256-CBC 加密\n   - 没有 secret 密钥，前端无法生成有效 token\n\n2. **前端无法获取真实坐标**\n   - 坐标信息加密存储在 token 中\n   - 前端只能看到加密字符串，无法解密\n\n3. **token 可以公开传输**\n   - 即使 token 被拦截，没有 secret 也无法解密\n   - token 包含过期时间，自动失效\n\n### 优势\n\n- ✅ **无需 session/Redis**：服务器不需要存储状态\n- ✅ **水平扩展友好**：多个服务器实例都可以验证\n- ✅ **简单部署**：不需要额外的存储服务\n- ✅ **API 简洁**：只需两个接口（生成 + 验证）\n\n### 权衡\n\n- ⚠️ token 可以多次使用（在过期时间内）\n- 💡 如需防止重放，可在应用层添加已使用token的记录（见安全建议）\n\n## 完整示例\n\n### Express 集成示例\n\n```typescript\nimport express from 'express';\nimport { generateCaptcha, verifyCaptcha } from '@aardpro/captcha-node';\n\nconst app = express();\napp.use(express.json());\n\nconst SECRET = 'your-app-secret-key-at-least-20-characters-long';\n\n// 生成验证码（无状态设计）\napp.get('/api/captcha', async (req, res) => {\n  try {\n    const { image, token } = await generateCaptcha({\n      chars: 'ABCDEFGHJKMNPQRSTUVWXYZ23456789',\n      count: 4,\n      secret: SECRET\n    });\n\n    // 将 image 和 token 都返回给前端\n    // token 是加密的，前端无法解密，也无法伪造\n    res.json({ image, token });\n  } catch (error) {\n    res.status(500).json({ error: '生成验证码失败' });\n  }\n});\n\n// 验证验证码（无状态设计）\napp.post('/api/verify', (req, res) => {\n  try {\n    const { input, token } = req.body;\n\n    if (!input || !token) {\n      return res.status(400).json({ error: '缺少必要参数' });\n    }\n\n    const isValid = verifyCaptcha({\n      token,    // 从前端接收的加密token\n      input,    // 用户点击的坐标\n      secret: SECRET\n    });\n\n    res.json({ valid: isValid });\n  } catch (error) {\n    res.status(500).json({ error: '验证失败' });\n  }\n});\n\napp.listen(3000);\n```\n\n### 前端示例（HTML + JavaScript）\n\n```html\n<!DOCTYPE html>\n<html>\n<head>\n  <title>验证码示例</title>\n</head>\n<body>\n  <div id=\"captcha-container\">\n    <img id=\"captcha-image\" src=\"\" alt=\"验证码\" style=\"cursor: pointer;\" />\n    <p>请按顺序点击图片上的字符（从左到右）</p>\n    <button onclick=\"submitCaptcha()\">提交</button>\n    <button onclick=\"refreshCaptcha()\">刷新</button>\n  </div>\n\n  <script>\n    let clicks = [];\n    let currentToken = '';\n\n    // 加载验证码\n    async function refreshCaptcha() {\n      clicks = [];\n      const response = await fetch('/api/captcha');\n      const data = await response.json();\n\n      // 保存token和显示图片\n      currentToken = data.token;\n      document.getElementById('captcha-image').src = data.image;\n    }\n\n    // 记录点击位置\n    document.getElementById('captcha-image').addEventListener('click', (e) => {\n      const rect = e.target.getBoundingClientRect();\n      const x = Math.round(e.clientX - rect.left);\n      const y = Math.round(e.clientY - rect.top);\n\n      clicks.push([x, y]);\n\n      // 可选：在点击位置显示标记\n      showClickMarker(e.clientX, e.clientY);\n    });\n\n    // 提交验证\n    async function submitCaptcha() {\n      const response = await fetch('/api/verify', {\n        method: 'POST',\n        headers: { 'Content-Type': 'application/json' },\n        body: JSON.stringify({\n          input: clicks,\n          token: currentToken  // 将token一起发送\n        })\n      });\n\n      const data = await response.json();\n\n      if (data.valid) {\n        alert('验证通过！');\n      } else {\n        alert('验证失败，请重试');\n        refreshCaptcha();\n      }\n    }\n\n    // 页面加载时获取验证码\n    refreshCaptcha();\n  </script>\n</body>\n</html>\n```\n\n## 安全建议\n\n1. **密钥管理**\n   - `secret` 应该存储在环境变量中，不要硬编码在代码里\n   - 使用足够长的密钥（至少20个字符）\n   - 定期更换密钥\n\n2. **防重放攻击**\n   - 本库采用无状态设计，token可以多次验证（只要未过期）\n   - 如需防止重放，建议在应用层实现：\n     - 使用 Redis 存储已使用的token（验证成功后set，过期时间与token一致）\n     - 或在token中添加nonce，验证成功后记录到数据库\n   - token本身有过期时间限制（默认5分钟）\n\n3. **防暴力破解**\n   - 在应用层面限制验证失败次数（如使用 Redis 计数）\n   - 失败次数过多时临时封禁 IP 或用户\n   - 不要返回具体的错误原因（如\"坐标错误\"或\"已过期\"）\n\n4. **过期时间**\n   - 根据业务需求设置合理的过期时间\n   - 默认 5 分钟适用于大多数场景\n   - 敏感操作建议使用更短的过期时间\n\n## 常见问题\n\n### Q: 安装失败怎么办？\n\nA: 本库使用预编译的二进制文件，通常不会出现安装问题。如果遇到问题：\n1. 确保使用的是 Node.js >= 18.0.0\n2. 尝试清除缓存：`npm cache clean --force`\n3. 删除 node_modules 后重新安装\n\n### Q: 如何提高验证码的安全性？\n\nA:\n1. 使用更长的字符池（包含中文、数字、字母混合）\n2. 增加字符数量（count 参数）\n3. 缩短过期时间\n4. 在应用层面添加频率限制\n\n### Q: 为什么验证总是失败？\n\nA: 检查以下几点：\n1. 确保用户按顺序点击（从左到右）\n2. 确保坐标格式正确 `[[x1, y1], [x2, y2], ...]`\n3. 确保 secret 字符串完全一致\n4. 检查 token 是否已过期\n\n### Q: 可以自定义背景图吗？\n\nA: 当前版本使用内置的 5 张背景图。如需自定义，可以修改源码中的 `src/images/backgrounds.ts` 文件。\n\n### Q: 支持哪些 Node.js 版本？\n\nA: 需要 Node.js >= 18.0.0\n\n## 开发\n\n```bash\n# 克隆仓库\ngit clone <repository-url>\n\n# 安装依赖\nnpm install\n\n# 构建\nnpm run build\n\n# 运行测试\nnpm test\n\n# 开发模式（监听文件变化）\nnpm run dev\n```\n\n## License\n\nMIT\n\n## 支持\n\n如有问题或建议，请提交 [Issue](https://github.com/aardpro/captcha-node/issues)。\n","readmeFilename":"README.md"}