
网络安全应用安全后端【免费下载链接】capFree, open-source and self-hosted CAPTCHA alternative to reCAPTCHA. Privacy-first and powered by proof-of-work and instrumentation challenges.项目地址https://gitcode.com/gh_mirrors/cap13/cap点击查看免费下载cap.js/server是 cap 项目免费、开源、可自托管的 reCAPTCHA 替代方案的早期服务端库负责在服务端创建、兑换与校验基于证明工作量proof-of-work的验证挑战。本文以该库的官方文档为主线完整讲解安装、Postgres 存储接入、三种主流后端框架的路由接线、全部公开方法与参数并结合仓库源码与测试深入剖析其底层机制最后给出迁移到无状态继任库capjs-core的对照方案。库的定位与历史现状在进入代码之前必须先说明cap.js/server在 cap 生态中的位置它是 cap 项目最早期的服务端实现采用有状态设计——挑战challenges与令牌tokens需要持久化到数据库并依赖文件系统或内存列表维护状态。官方文档在其页面顶部明确标注了弃用警告cap.js/server已不再是推荐的 Cap 服务端库新项目应改用capjs-core。后者是无状态实现不触碰文件系统、无内存令牌列表可在 Cloudflare Workers / Lambda / 边缘函数上运行使用签名 JWT 承载挑战配置并且是 Cap Standalone 在内部实际依赖的库。不过了解cap.js/server依然有价值它是理解 cap 挑战机制演进的最佳入口其存储模型、挑战参数语义与校验流程在capjs-core中一脉相承。若你维护的是早期接入的存量系统本文的迁移章节可以直接指导你完成过渡。安装cap.js/server通过你习惯的包管理器安装支持 Bun、npm 与 pnpmbun add cap.js/servernpm i cap.js/serverpnpm i cap.js/server安装完成后默认导出Cap类所有核心方法均围绕该类的实例展开。快速上手数据库建表与 Cap 实例配置cap.js/server是有状态库因此需要一个数据库来存储挑战与令牌。官方文档以 Bun 内置的 SQL 模块连接 Postgres 为例给出了完整的建表与配置代码。1. 建立数据库连接并建表import Cap from cap.js/server; import { SQL } from bun; const db new SQL(postgres://user:passwordlocalhost:5432/dbname); await db CREATE TABLE IF NOT EXISTS challenges ( token TEXT PRIMARY KEY, data JSONB NOT NULL, expires BIGINT NOT NULL ); ; await db CREATE TABLE IF NOT EXISTS tokens ( key TEXT PRIMARY KEY, expires BIGINT NOT NULL ); ;两张表分别对应库的两个存储域challenges保存尚未解决的挑战。token作为主键唯一标识一次挑战data以 JSONB 保存挑战内容expires记录过期时间戳毫秒。tokens保存已兑换redeem成功后发放的验证令牌。key为主键expires同样为毫秒时间戳。从字段设计可以推断库自身不负责清理数据过期判断由存储层在读写时显式完成——这正是后续read、get等方法中都要附带expires Date.now()条件的原因。2. 构造 Cap 实例并注入存储实现const cap new Cap({ storage: { challenges: { store: async (token, challengeData) { await db INSERT INTO challenges (token, data, expires) VALUES (${token}, ${challengeData}, ${challengeData.expires}) ON CONFLICT (token) DO UPDATE SET data EXCLUDED.data, expires EXCLUDED.expires ; }, read: async (token) { const [row] await db SELECT data, expires FROM challenges WHERE token ${token} AND expires ${Date.now()} LIMIT 1 ; return row ? { challenge: row.data, expires: Number(row.expires) } : null; }, delete: async (token) { await db DELETE FROM challenges WHERE token ${token} ; }, deleteExpired: async () { await db DELETE FROM challenges WHERE expires ${Date.now()} ; }, }, tokens: { store: async (tokenKey, expires) { await db INSERT INTO tokens (key, expires) VALUES (${tokenKey}, ${expires}) ON CONFLICT (key) DO UPDATE SET expires EXCLUDED.expires ; }, get: async (tokenKey) { const [row] await db SELECT expires FROM tokens WHERE key ${tokenKey} AND expires ${Date.now()} LIMIT 1 ; return row ? Number(row.expires) : null; }, delete: async (tokenKey) { await db DELETE FROM tokens WHERE key ${tokenKey} ; }, deleteExpired: async () { await db DELETE FROM tokens WHERE expires ${Date.now()} ; }, }, }, }); export default cap;几个值得注意的实践要点store使用ON CONFLICT ... DO UPDATE实现 upsert 语义避免同一 token 重复插入报错read与get都显式过滤掉已过期记录过期数据由deleteExpired定期清扫read返回的对象结构{ challenge, expires }是库约定好的形状实现时必须保持一致store接收的challengeData对象中直接带有expires字段毫秒时间戳可直接落库。后端路由接线Elysia、Express 与 Fastify配置好存储后需要把 Cap 的能力暴露成 HTTP 路由供前端 widget 调用。官方文档给出了一条通用约定Cap API 位于/cap/路径下前端通过 widget 的data-cap-api-endpoint属性指向该地址详见 widget 文档。Elysiaimport { Elysia } from elysia; import cap from ./cap.js; new Elysia() .post(/cap/challenge, async () { return await cap.createChallenge(); }) .post(/cap/redeem, async ({ body, set }) { const { token, solutions } body; if (!token || !solutions) { set.status 400; return { success: false }; } return await cap.redeemChallenge({ token, solutions }); }) .listen(3000);Expressimport express from express; import cap from ./cap.js; const app express(); app.use(express.json()); app.post(/cap/challenge, async (req, res) { res.json(await cap.createChallenge()); }); app.post(/cap/redeem, async (req, res) { const { token, solutions } req.body; if (!token || !solutions) { return res.status(400).json({ success: false }); } res.json(await cap.redeemChallenge({ token, solutions })); }); app.listen(3000);注意 Express 需要显式挂载express.json()中间件才能解析 JSON 请求体。Fastifyimport Fastify from fastify; import cap from ../cap.js; const fastify Fastify(); fastify.post(/cap/challenge, async (req, res) { res.send(await cap.createChallenge()); }); fastify.post(/cap/redeem, async (req, res) { const { token, solutions } req.body; if (!token || !solutions) { return res.code(400).send({ success: false }); } res.send(await cap.redeemChallenge({ token, solutions })); }); fastify.listen({ port: 3000 });三个框架的路由逻辑完全一致可以提炼出两条接口契约POST /cap/challenge无需任何入参返回cap.createChallenge()的结果POST /cap/redeem接收{ token, solutions }二者缺一即返回400否则返回cap.redeemChallenge({ token, solutions })的结果。验证令牌并继续业务逻辑当用户在前端完成 CAPTCHA 并把令牌提交回你的后端时你需要在处理业务之前先校验令牌const { success } await cap.validateToken(...); if (!success) throw new Error(invalid cap token); // ...your logic这里validateToken校验的是挑战被兑换后发放的令牌由redeemChallenge写入tokens表而不是挑战本身的 token。只有校验通过后才应该继续执行注册、发帖、登录等敏感逻辑。方法与参数全解new Cap({ ... })构造函数的完整参数如下{ disableAutoCleanup: false, storage: { challenges: { store: async (token, challengeData) {}, read: async (token) {}, delete: async (token) {}, deleteExpired: async () {} }, tokens: { store: async (tokenKey, expires) {}, get: async (tokenKey) {}, delete: async (tokenKey) {}, deleteExpired: async () {} } }, state: { challengesList: {}, tokensList: {} } // obsolète : // utilisé pour le stockage clé-valeur en JSON // tokens_store_path: .data/tokensList.json, // désactive toutes les opérations sur le système de fichiers, généralement utilisé avec la modification de létat // noFSState: false, }参数语义disableAutoCleanup默认false库默认会自动清理过期挑战与令牌调用内部cleanup()置为true可关闭自动清理由你手动安排清理时机。storage必填的存储适配层challenges与tokens两个域各自需要store/read(或get) /delete/deleteExpired四个异步方法。state.challengesList与state.tokensList内存态中的挑战与令牌列表可结合noFSState使用实现纯内存运行。tokens_store_path已废弃早期版本用于指定 JSON 键值文件的存储路径如.data/tokensList.json。noFSState已废弃置true可禁用全部文件系统操作通常配合直接编辑state使用。此外你可以在任意时刻通过cap.config对象读取或修改Cap实例的选项例如动态调整难度或存储实现。await cap.createChallenge({ ... })创建新挑战。参数与默认值{ challengeCount: 50, challengeSize: 32, challengeDifficulty: 4, expiresMs: 600000 }challengeCount一次挑战包含的证明工作量子题数量默认50challengeSize盐值的长度十六进制字符数默认32challengeDifficulty目标前缀长度十六进制字符数默认4数值越大目标前缀越长、求解越难expiresMs挑战有效期毫秒默认60000010 分钟。响应{ challenge, token, expires }其中challenge形如{ c, s, d }分别对应上述三个难度参数token是客户端求解后提交时使用的挑战令牌expires是挑战过期的时间戳毫秒。有意思的是这些默认值与继任库capjs-core中generateChallenge的默认值完全一致——在 capjs-core 入口实现 中可以找到同名的默认常量DEFAULT_CHALLENGE_COUNT 50、DEFAULT_CHALLENGE_SIZE 32、DEFAULT_CHALLENGE_DIFFICULTY 4、DEFAULT_CHALLENGE_TTL_MS 10 * 60 * 1000。这说明挑战参数语义在库的换代过程中被完整保留。cap.redeemChallenge({ ... })客户端提交求解结果兑换验证令牌。入参{ token: 客户端回传的挑战 token, solutions: 客户端求解出的答案数组 }响应{ success, token }success表示兑换是否成功成功后返回的token是写入tokens存储的验证令牌供后续validateToken使用。solutions的数组长度必须与createChallenge时的challengeCount一致服务端会逐个子题校验证明工作量的哈希是否满足目标前缀。await cap.validateToken(..., { ... })校验兑换后的令牌。入参{ keepToken: false }keepToken默认false置true时校验后保留该令牌不删除适用于短时间窗口内需要重复校验的场景默认false意味着校验成功后令牌即被消费删除防止重放。响应{ success }await cap.cleanup()清理所有已过期的挑战与令牌。正常情况下库会自动执行由disableAutoCleanup控制无需手动调用仅当关闭了自动清理或在部署无守护进程的边缘环境时才需要自行调度。源码视角挑战参数的边界与校验顺序继任库capjs-core的实现保留了与原库一致的挑战语义其入口 core/src/index.js 是对理解校验流程最直接的参考。参数边界在 generateChallenge 实现 中可以看到challengeCount与challengeSize必须是正整数且分别不超过1000与256challengeDifficulty必须在[1, 16]区间内常量定义见 core/src/index.js#L84-L87。同时secret必须为字符串或 Buffer 且长度不小于 16 字节assertSecret这些约束在单元测试中均有覆盖例如 core.test.js 验证了缺失、过短、类型错误的 secret 均会被拒绝。校验顺序与失败原因从 validateChallenge 实现 可以还原出完整校验流水线先校验请求体形状非对象、缺 token、缺 solutions再验签 JWT随后按序检查 scope 匹配、是否过期、子题数量与数值类型、每道子题的哈希前缀匹配最后才是 instrumentation 结果与防重放 nonce 消费。校验失败时会返回结构化的reason常见取值包括reason含义invalid_body请求体不是对象missing_token缺少挑战 tokenmissing_solutionssolutions 缺失或不是数组invalid_tokenJWT 签名无效 / 格式错误 / 参数越界scope_mismatchtoken 的 scope 与校验时传入的 scope 不一致expired挑战 JWT 已过期invalid_solutionssolutions 长度不符或含非数字invalid_solution证明工作量哈希不满足目标前缀instr_*浏览器行为检测instrumentation未通过nonce_store_error防重放回调抛错already_redeemed防重放回调判定该提交已被消费过防重放校验被刻意放在证明工作量和 instrumentation 校验之后——这是有意的设计如果攻击者重放一段被拦截的提交但附带伪造的 solutions就不会误烧掉合法用户尚未使用的 nonce详见 capjs-core 文档 的 Anti-replay 一节。迁移到无状态库 capjs-core官方强烈建议新项目直接使用capjs-core。两者核心差异如下表源自 capjs-core 文档 的对照表维度cap.js/servercapjs-core状态内存 磁盘/数据库存储令牌无状态挑战令牌即签名 JWT构造方式new Cap({ ... })无构造函数每次调用传入secret防重放内置令牌列表 周期性清理可选通过consumeNonce回调接入你的存储清理钩子SIGINT/beforeExit时落盘无TTL 编码在 JWT 的exp中文件系统需要用于持久化完全不触碰Workers 兼容否依赖文件系统是capjs-core 的基本用法import { generateChallenge, validateChallenge } from capjs-core; // 长、随机、高熵的密钥在各进程间保持一致 const SECRET process.env.CAP_SECRET; // 1) 服务端路由创建挑战 const ch await generateChallenge(SECRET, { scope: signup, // 可选 instrumentation: true, // 可选 }); // → { challenge: { c, s, d }, token, expires, instrumentation? } // 2) 服务端路由校验已解决的挑战 const result await validateChallenge( SECRET, { token: req.body.token, solutions: req.body.solutions, instr: req.body.instr, }, { scope: signup, consumeNonce: async (sigHex, ttlMs) myStore.setIfNotExists(cap:${sigHex}, 1, ttlMs), }, ); if (result.success) { // result.token, result.tokenKey, result.expires, result.scope }令牌语义的迁移要点与旧库最大的行为差异是capjs-core不再替你维护兑换令牌列表它返回一个tokenKey让你自行存储同时返回一个token交给用户。后续校验时你需要从用户提交的 token 重新派生 key 再查存储import { createHash } from node:crypto; // 校验路由 const [id, verToken] req.body.token.split(:); const tokenKey ${id}:${createHash(sha256).update(verToken).digest(hex)}; const expires await myStore.get(cap-token:${tokenKey}); if (!expires || Number(expires) Date.now()) { return res.status(401).end(); }仓库内的真实迁移样本Cap Standalone如果你希望看到从旧库心智模型平滑迁移的完整参考实现可以直接阅读 Standalone 的挑战路由它在/redeem中调用coreValidateChallenge用 ValkeyRedis 协议的SET NX EX实现consumeNonce防重放并通过signToken回调自定义兑换令牌格式为${siteKey}:${redeemId}:${redeemSecret}随后把该令牌连同过期时间写入token:键。对应的消费侧在 siteverify.js 中POST /siteKey/siteverify校验secret后用GETDEL一次性取出并删除token:response从而实现令牌的单次消费语义——这正是旧库校验即删除行为在无状态架构下的等价物。总结cap.js/server为 cap 生态奠定了服务端挑战的完整范式存储适配器驱动的new Cap({ ... })构造方式、createChallenge→redeemChallenge→validateToken三段式生命周期、可关闭的自动清理机制。虽然它已让位于无状态的capjs-core但两者的挑战参数语义、校验顺序与安全边界一脉相承存量用户可依据本文的迁移对照将存储与防重放逻辑平滑平移至新库而新项目则应直接从capjs-core文档 起步或使用开箱即用的 Cap StandaloneDocker 自托管内含站点密钥仪表盘与 reCAPTCHA 兼容的 siteverify 接口。赞分享网络安全应用安全后端【免费下载链接】capFree, open-source and self-hosted CAPTCHA alternative to reCAPTCHA. Privacy-first and powered by proof-of-work and instrumentation challenges.项目地址https://gitcode.com/gh_mirrors/cap13/cap点击查看免费下载相关推荐Cap 服务端库 cap.js/server 实战指南挑战生成、Token 验证与 capjs-core 迁移路径Cap 服务端库 cap.js/server 实战指南挑战生成、Token 验证与 capjs core 迁移路径 cap.js/server 是开源自托网络安全应用安全后端Cap 服务端接入实战用 cap.js/server 创建与校验 PoW 挑战Cap 服务端接入实战用 cap.js/server 创建与校验 PoW 挑战 本文围绕 Cap 仓库中的服务端接入指南 docs/guide/serve网络安全应用安全后端capjs-core 无状态 CAPTCHA 挑战生成与校验Cap 服务端核心库的完整 API 实战指南capjs core 无状态 CAPTCHA 挑战生成与校验Cap 服务端核心库的完整 API 实战指南 导读 capjs core 是自托管 CAPTCHA网络安全应用安全后端上一篇如何安装和配置tsuTermux root权限管理工具快速入门教程下一篇gh_mirrors/yo/you-dont-know-js-ru部署指南本地搭建JavaScript学习环境创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考