- 网络安全
- 应用安全
- 后端
【免费下载链接】cap
Free, open-source and self-hosted CAPTCHA alternative to reCAPTCHA. Privacy-first and powered by proof-of-work and instrumentation challenges.
导读
Cap 是一个免费、开源、可自托管的 reCAPTCHA 替代方案,其验证核心不是让用户点选图片或识别扭曲文字,而是让浏览器在后台默默完成工作量证明(proof-of-work)质询。本文围绕仓库中的演示页面 docs/zh/guide/demo.md 展开:先说明该页面如何以“评论表单”的形式让访客零配置体验整个验证闭环,再深入剖析演示服务端 demo/index.js 的完整实现——从挑战签发、Nonce 防重放、令牌赎回,到 widget 前端配置与浮动态模式,最终给出本地一键运行演示的方法。读完本文,你将掌握 Cap 演示的端到端工作链路,并能在自己的机器上跑起同款演示环境。
一、演示页面是什么:一个内嵌在文档里的“活”验证组件
docs/zh/guide/demo.md的 frontmatter 用一句话定义了它的主题:“在线体验 Cap:实时演示这个开源、自托管的 CAPTCHA 验证组件如何在你的浏览器中求解工作量证明质询,无需点选任何视觉谜题。”
与普通纯文字教程不同,这份文档本质上是一个可交互的演示页,页面上嵌入了几类 VitePress 组件占位符:
<DemoTabs />:用于切换不同的演示模式(如普通模式与浮动态模式);<Demo />:真正渲染cap-widget验证组件的挂载点,文档通过内联样式--cap-widget-width: 300px限定演示组件的宽度;<DemoReply />:在验证完成后展示“回复已发送”之类的反馈区域。
页面还自带了一段完整的评论表单 CSS(.login-form、.signin-button等样式),用纯 CSS 模拟出一个真实评论框 + 提交按钮的交互外观:文本框获得焦点时出现品牌色光环(box-shadow: 0 0 0 .25rem rgba(38, 181, 250, .25)),未激活的提交按钮呈灰度禁用态,激活后带两次脉冲动画(signin-button-pulse)。这些样式全部内联在 Markdown 文档中,意味着演示页在视觉上被刻意塑造成“发一条评论”的真实场景,让访客在不知不觉中完成一次完整的 CAPTCHA 验证体验。
页面最后一行明确标注了演示的边界:“本演示使用的占位服务端不会执行 instrumentation(浏览器环境检测)。”这是理解演示能力范围的关键信息——文档页面上跑的是一个简化版后端,只演示工作量证明质询的求解,不演示 instrumentation 挑战的完整流程。
二、演示体验流程:从评论框到验证令牌
演示页面的交互逻辑可以拆成三步,与真实生产环境中的 Cap 验证闭环一致:
- 填写评论:用户在仿评论表单的文本框中输入内容(页面预置了示例文案“看起来很棒,感谢分享!”);
- 触发验证:
<Demo />挂载的cap-widget组件向演示服务端的POST /api/challenge请求签发挑战,随后在浏览器内(借助 WASM 加速)后台求解工作量证明质询,全程无需点击任何图片、无需识别任何字符; - 提交与反馈:验证通过后组件抛出
solve事件携带令牌,<DemoReply />区域给出完成反馈;提交按钮从禁用态切换为激活态并播放脉冲动画。
整个流程中,用户与验证组件的交互负担被压缩到最低——这是 Cap 与 reCAPTCHA 类视觉谜题方案最核心的体验差异。下面我们从源码层面看演示服务端如何支撑这条链路。
三、演示服务端实现解剖:demo/index.js
演示服务端基于Elysia(Bun 生态的 Web 框架)编写,入口文件为 demo/index.js,共约 130 行,职责清晰,可按功能分为四块。
3.1 全局状态:Secret、Nonce 与已赎回令牌
const SECRET = process.env.CAP_SECRET || randomBytes(32).toString("hex"); const redeemed = new Map(); const nonces = new Map();SECRET:用于签发/校验挑战的 JWT 密钥。演示模式未设置CAP_SECRET环境变量时,会为每次进程启动随机生成 32 字节密钥——这直接导致重启服务后旧令牌全部失效,仅适合演示场景;生产环境则必须使用稳定、高熵的固定密钥(见 core/src/index.js 对secret长度至少 16 字节的强制校验)。redeemed与nonces是两个内存 Map,分别记录已赎回令牌的过期时间与已消费 Nonce 的 TTL,配合每 60 秒一次的定时清理(.unref()不阻塞进程退出)实现内存态防重放。
3.2 静态资源与打包路由
app.get("/cap.js", async ({ set }) => { const main = await fs.readFile("../widget/src/src/cap.js", "utf-8"); const worker = await fs.readFile("../widget/src/src/worker.js", "utf-8"); const css = await processCSS(); const bundle = main .replace("%%workerScript%%", () => JSON.stringify(worker)) .replace("%%capCSS%%", () => css) .replace("%%i18nKeys%%", () => i18nShippedKeys.join(",")) .replace("%%i18nData%%", () => i18nJSON); ... });/直接返回index.html;/cap.js在运行时完成 widget 源码的打包:把 widget/src/src/cap.js 中的%%workerScript%%(求解 worker)、%%capCSS%%(经 lightningcss)以及%%i18nKeys%%/%%i18nData%%(多语言文案,来源 widget/src/src/i18n/translations.js)等占位符逐一替换为真实内容;/cap.css返回压缩后的组件样式;/cap-floating.js直接透传浮动态入口 widget/src/src/cap-floating.js。
也就是说,演示页面引用的cap.js是从当前仓库的 widget 源码现场打包的,任何 widget 改动都会即时反映在演示中。
3.3 三个 API 端点:完整的验证闭环
演示服务端用三个端点复刻了 Cap 的生产验证流程:
| 端点 | 方法 | 职责 |
|---|---|---|
/api/challenge | POST | 调用generateChallenge(SECRET, { instrumentation: true })签发挑战 |
/api/redeem | POST | 调用validateChallenge(SECRET, body, { consumeNonce })校验求解结果并发放令牌 |
/api/validate | POST | 接收{ token },校验令牌是否已赎回且未过期 |
/api/challenge的实现最简洁,直接委托给 capjs-core:
app.post("/api/challenge", async () => { return await generateChallenge(SECRET, { instrumentation: true, }); });注意这里instrumentation: true——虽然页面文案声明“占位服务端不会执行 instrumentation”,但挑战签发端仍请求了 instrumentation 数据(由 core/src/index.js 的generateChallenge通过generateInstrumentation生成并经encryptGcm加密进 JWT)。结合演示注释可推断:演示服务端签发了带 instrumentation 的挑战,但占位服务端在校验侧并未真正执行对应的环境检测环节,即验证只依赖工作量证明部分。
/api/redeem则完整演示了防重放与令牌发放:
app.post("/api/redeem", async ({ body, set }) => { if (!body || typeof body !== "object") { set.status = 400; return { success: false, reason: "invalid_body" }; } const result = await validateChallenge(SECRET, body, { consumeNonce }); if (result.success) { redeemed.set(result.tokenKey, result.expires); ... } return result; });consumeNonce回调实现如下,基于挑战令牌的签名哈希做 TTL 级去重:
const consumeNonce = async (sigHex, ttlMs) => { if (nonces.has(sigHex)) return false; nonces.set(sigHex, Date.now() + ttlMs); return true; };这对应 core/src/index.js 中validateChallenge对opts.consumeNonce的调用约定:同一挑战令牌只能赎回一次,重复提交返回already_redeemed。
/api/validate负责令牌二次核验——这模拟了生产环境中“前端拿到令牌后,业务后端再向 Cap 服务端确认令牌有效”的场景:
const [id, secret] = String(body.token).split(":"); const tokenKey = `${id}:${createHash("sha256").update(secret).digest("hex")}`; const expires = redeemed.get(tokenKey); if (!expires || expires < Date.now()) return { success: false }; return { success: true, expires };令牌格式id:secret与validateChallenge成功路径的返回值一致(core/src/index.js):服务端只持久化id + secret 的 SHA-256,明文 secret 不下存储,过期即失效。
四、挑战生成与校验的底层参数:capjs-core 默认值
演示端点使用的generateChallenge/validateChallenge均来自capjs-core包(demo/package.json 中声明依赖capjs-core: ^0.1.0),其实现位于 core/src/index.js。演示未显式传参,因此实际生效的是以下默认值:
| 参数 | 默认值 | 说明 |
|---|---|---|
challengeCount(c) | 50 | 单次挑战含 50 个子质询,求解器需逐一完成 |
challengeSize(s) | 32 | 每个质询 salt 的长度(字节) |
challengeDifficulty(d) | 4 | 工作量证明难度,即哈希结果需匹配的前导零十六进制位数 |
expiresMs | 10 * 60 * 1000(10 分钟) | 挑战令牌 TTL |
tokenTtlMs | 20 * 60 * 1000(20 分钟) | 成功验证后发放的令牌 TTL |
代码层面的取值范围约束(core/src/index.js):challengeCount ≤ 1000、challengeSize ≤ 256、challengeDifficulty ∈ [1, 16],越界会直接抛错。验证侧会基于挑战 JWT 中的c/s/d参数逐项重算并比对 SHA-256 前缀匹配(powMatchesPrefix),确保求解结果真实有效。
求解的核心逻辑是:对每个质询,以“令牌哈希 + 序号”派生盐与目标值,服务端用prngFromHash生成确定性的 salt 与 target,客户端提交数字解solutions[i],服务端计算sha256(salt + solution)并校验其十六进制前缀是否命中 target(core/src/index.js)。这正是“无需视觉谜题、纯算力证明”的技术本质。
五、前端 widget 配置:demo/index.html里的真实用法
演示页的 HTML 骨架在 demo/index.html 中,它同时展示了普通模式与浮动态模式两种<cap-widget>用法:
<cap-widget id="cap" onsolve="console.log(`Token: ${event.detail.token}`)" ># 安装依赖(demo 与 widget 各自有独立的 bun.lock,分别安装) bun install --cwd demo bun install --cwd widget bun install --cwd core # 启动演示服务 bun run --cwd demo start服务默认监听http://localhost:3000,可通过环境变量调整:
PORT=8080 bun run --cwd demo start # 修改端口 CAP_SECRET="你的高熵密钥" bun run --cwd demo start # 使用固定密钥启动后浏览器访问首页即可获得与文档页一致的体验:填写评论 → widget 在后台完成工作量证明 → 控制台打印Token: ...,点击 “Trigger floating mode” 可切换浮动态组件。
需要特别说明的适用前提与限制:
- 运行演示需要 Bun 运行时;Node.js 直接执行
index.js会因 Elysia 依赖与bun语法而不保证兼容; - 演示的令牌存储(
redeemed/nonces)均为进程内存态,重启即失效,仅适合本地体验; - 如前文所述,占位服务端的校验侧不真正执行 instrumentation 环境检测,完整的 instrumentation 挑战(含自动化浏览器拦截等)需要在生产级部署(如 standalone 或自行接入 capjs-core)中验证。
七、从演示到生产:演示代码透露的落地要点
演示服务端虽然只有一百余行,却完整覆盖了生产接入的三件套,值得作为最小可运行参考:
- 挑战签发与校验必须共享同一
SECRET:演示默认每次启动随机生成,生产场景应使用固定密钥并在多实例间保持一致(assertSecret强制 ≥ 16 字节); - 防重放是刚需:通过
consumeNonce回调按签名哈希去重,返回already_redeemed,这是防止令牌被多次使用的基础设施; - 令牌核验接口解耦:
/api/validate展示了“业务侧只存 tokenKey、不存明文”的令牌校验范式,避免业务系统直接依赖挑战校验细节。
将这三条迁移到自己的服务端,就足以把 Cap 从“可玩”的演示推进到“可用”的接入状态。
结语
docs/zh/guide/demo.md表面上是一页交互演示,背后却是一条完整的“挑战签发 → 浏览器求解 → 赎回令牌 → 业务核验”链路。本文结合 demo/index.js、demo/index.html 与 core/src/index.js 逐层还原了它的实现:默认参数、Nonce 防重放、运行时打包、浮动态配置与本地启动方法,全部有仓库源码可查。理解这套最小闭环之后,无论是接入 widget、自建服务端,还是阅读 docs/zh/guide/capjs-core.md 与 docs/zh/guide/server.md 做生产级部署,你都将有一个直观的“工作模型”作为起点。
- 网络安全
- 应用安全
- 后端
【免费下载链接】cap
Free, open-source and self-hosted CAPTCHA alternative to reCAPTCHA. Privacy-first and powered by proof-of-work and instrumentation challenges.
相关推荐
Cap 在线 Demo 全解析:零视觉谜题的 proof-of-work CAPTCHA 演示与实现
Cap 在线 Demo 全解析:零视觉谜题的 proof of work CAPTCHA 演示与实现 本文围绕 Cap 开源仓库中 docs/fr/guide/
网络安全应用安全后端Cap 在线演示(Demo)深度解析:体验与复现免点击、基于工作量证明的 CAPTCHA 验证
Cap 在线演示(Demo)深度解析:体验与复现免点击、基于工作量证明的 CAPTCHA 验证 本文以仓库多语言文档中的 演示页(泰语版) https://li
网络安全应用安全后端PPTist在线演示工具:重新定义浏览器中的PPT创作体验
PPTist在线演示工具:重新定义浏览器中的PPT创作体验 你是否曾经遇到过这样的困境:急需制作一份专业PPT,却发现电脑上没有安装PowerPoint?或者想
前端企业应用
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考