- 网络安全
- 应用安全
- 后端
【免费下载链接】cap
Free, open-source and self-hosted CAPTCHA alternative to reCAPTCHA. Privacy-first and powered by proof-of-work and instrumentation challenges.
Cap 是一个免费、开源、可自托管的 CAPTCHA 替代方案。本文围绕其官方文档 docs/zh/guide/effectiveness.md 的核心论断展开:Cap 不靠猜测"谁是人类"来拦截机器人,而是通过**工作量证明(Proof of Work, PoW)与Instrumentation(浏览器环境检测)**两套独立机制,把自动化滥用的成本抬到经济上无利可图,同时让真实用户的体验保持快速且几乎无感。读完本文,你将理解 PoW 的攻防经济学、为什么 Cap 默认采用抗 GPU 的 HashWX 协议、这两层验证在源码中如何实现,以及如何在 cap-core 与 Standalone 中实际配置难度与协议。
隐私与安全:默认不追踪、默认防重放
Cap 在隐私层面的设计是"少即是多",这一点直接写进了有效性文档:
- 默认不使用 Cookie,也没有任何形式的遥测:质询的签发与验证不依赖任何客户端状态存储;
- 完全自托管:质询生成、验证、令牌签发全部发生在你自己的服务器上,没有任何数据被收集或存储到中心化服务器;
- 默认内置重放防护和基于签名的质询令牌:质询令牌是使用 HMAC 主密钥签名的 JWT,客户端无法伪造或篡改参数(难度、盐、过期时间),详见 core/src/index.js 中
generateChallenge的jwtSign调用。
从源码结构看,"无状态"是这套设计的根基:capjs-core库本身不持有任何会话,generateChallenge(secret, opts)每次调用都用crypto.randomBytes生成随机盐并把质询配置签进 JWT,validateChallenge(secret, body, opts)则先用jwtVerify还原并校验载荷(core/src/index.js)。重放防护以可插拔回调consumeNonce(sigHex, ttlMs)的形式提供——库不绑定具体存储,你可以把它接到 Redis 的SET NX EX、Cloudflare KV 或 PostgreSQL 唯一约束上,实现真正的按需开启(参见 docs/zh/guide/capjs-core.md 的"重放防护"一节)。
为什么选择工作量证明:把"猜谁是人"换成"让滥用变贵"
有效性文档开门见山承认一个事实:任何 CAPTCHA 最终都能被破解——无论是通过 AI 识别图片、算法逆向与伪造指纹,还是直接雇 CAPTCHA 打码平台人工解题。攻防双方因此陷入无休止的猫鼠游戏。Cap 的立场是:真正的区别不在于"能不能破",而在于攻击者要付出多少成本。
文档给出了一个很直观的经济学模型:
设想发送 10,000 条垃圾消息的成本是 1 美元,潜在收益 10 美元,这样有利可图。如果 Cap 抬高计算成本,把发送这批消息的成本变成 100 美元,垃圾消息发送者就要亏损 90 美元,经济动机就此消失。
这就是 PoW 的定位:通过要求付出计算量来阻止滥用,而不是依赖机器人不断学会模仿的人工验证手段。Cap 的工作量证明深受 Hashcash 启发;其 instrumentation 质询则借鉴了 Twitter 和 YouTube 各自的自定义质询。
从实现角度看,这套"成本论"对应的是 core/src/index.js 里validateChallenge对解的校验:每个提交的解必须满足powMatchesPrefix(sha256Bytes(salt + nonce), target),任何错误解都会以invalid_solution失败,攻击者无法靠"猜"通过。机器人要规模化地通过,就只能规模化地消耗 CPU 去求解——这正是成本的来源。
让 GPU 无用武之地:从 SHA-256 到 HashWX
作为通用 PoW 算法,SHA-256 是合理的选择,但它有一个致命弱点:GPU 每秒能清掉的数量大约是 CPU 的 150 倍。GPU 用成千上万条通道同步运行同一个固定函数,而机器人防护真正在意的指标恰恰是吞吐量——攻击者不关心单个质询要花多久,只关心每小时能清掉多少个。
因此,新建的密钥默认使用 HashWX,一个由 tevador(RandomX 与 HashX 的作者)设计的抗 GPU 哈希算法。Cap 在 wasm/src 中内置了其官方 WebAssembly 参考实现,并在 core/src/hashwx.js 中通过hashwxReady()懒加载编译。文档给出的吞吐量对比(来自 tevador 的未公开 CUDA 实现,RSW 行由 Cap 独立复现验证):
| 算法 | CPU(Ryzen 3700X,16 线程) | GPU(RTX 5060 Ti) | GPU 优势 |
|---|---|---|---|
| SHA-256 | 41 MH/s | 6150 MH/s | ~150x |
| RSW | 26 H/s | 4400 H/s | ~170x |
| HashWX | 2.8 MH/s | 5.8 MH/s | ~2x |
注意 RSW(时间锁谜题)也在表中:它在延迟维度上很出色(单个谜题内部的顺序平方无法并行),但在吞吐量维度上输得很惨,GPU 可以同时跑几千个互不相关的谜题,优势反而高达 ~170 倍。RSW 已被弃用:它仍然可以按密钥选用,现有密钥也继续可用,但不应再用于新的部署。
HashWX 协议如何工作
在 core/src/hashwx.js 中可以看到协议的完整实现脉络:
- 铸造:服务端取 32 个随机字节作为质询
C(crypto.randomBytes(HASHWX_CHALLENGE_SIZE)),再定一个难度d。没有密钥材料、没有预计算,铸造就是一次随机读取加一次 JWT 签名——这也解释了文档所说"HashWX 不需要密钥材料,启动时也不用做任何准备"。 - 客户端求解:客户端要找到一个 64 位 nonce
N,使得H(N) <= (2^64 - 1) / d,其中H = hashwx_make(sha256(C || u64le(N / n)))。每一块n个连续 nonce 共用一个生成出来的哈希函数。hashwxTarget()在源码中即U64_MAX / d的实现。 - 块大小
n:Cap 使用n = 65536(DEFAULT_HASHWX_NONCES_PER_HASH)。参考协议给原生客户端用的是 463;在浏览器里,每一块都要通过WebAssembly.Module把函数重新 JIT 编译一遍,更大的块能摊薄这部分开销。tevador 明确指出这个取舍:每个函数覆盖的 nonce 越多,协议就越容易被 JIT 编译的 GPU 内核追上,在此取值下撑住抗 GPU 能力的是分支发散。 - 服务端验证:服务端根据
C和提交的 nonce 对应的块索引重新算出种子(hashwxSeed即sha256(C || u64le(block))),生成那一个哈希函数,运行一次,再与目标比较。程序生成的开销大约只有 HashX 的 1/5,这正是验证能保持在几十微秒的原因。核心实现在 core/src/hashwx.js 的verifyHashwxSolution。
为什么 GPU 快不起来
HashWX 的四个抗 GPU 特性(均出自 tevador 的设计文档)值得展开:
- 分支发散:每个实例是 32 个程序,每个程序都是一个循环,以 1/2 的概率跳回自己的开头,算下来每次哈希正好 256 次分支。CPU 上这只是几次分支预测失败;GPU 上它会把一个 warp 拆成若干发散路径,只能一条接一条地执行。
- 刻意不对齐的 16 KB 暂存区:CPU 把它放进 L1,用乱序执行掩盖 3~4 个周期的延迟;GPU 只能放在由 L2 支撑的本地内存里,延迟约 100 个周期,且大多数 GPU 架构还得把两次相邻读取拼起来才能模拟不对齐读取。
- 深/浅源寄存器列表:源寄存器取自交错排列的"浅"列表和"深"列表,CPU 平均要处理 2.75 次相互依赖的读取;GPU 解释器只能按深列表特化,因为约 95% 的情况下一个 warp 里至少有一个线程在跑深程序,于是每次都吃下完整的 6 次依赖读取链。
- 受限指令集:指令集被限制在 WebAssembly 1.0 范围内(64 位乘、加、减、XOR、OR、循环移位和移位,6 位立即数),正是这一点让同一套算法能在浏览器里跑起来。
成本:服务端与客户端
文档给出了 Apple M3 单核心上validateChallenge实测的服务端成本(200 次铸造 + 40 次验证取中位数):
| 协议 | 铸造 | 验证 | 合计 |
|---|---|---|---|
| HashWX,1 个质询 | 14 µs | 40 µs | 54 µs |
| HashWX,4 个子质询(默认) | 20 µs | 129 µs | 149 µs |
| SHA-256(50 个质询,难度 4) | 4 µs | 83 µs | 87 µs |
| RSW(t = 75,000) | 1522 µs | 14 µs | 1536 µs |
单个 HashWX 质询是三者中往返开销最低的;默认的四个子质询约 150 µs,只有 RSW 的十分之一(RSW 每次签发都要做四次真正的模幂运算)。难度不影响服务端成本:无论找到解有多难,验证每个子质询都只需算一次哈希——这一点与 core/src/hashwx.js 中verifyHashwxSolution只执行单次hashwx_exec的实现完全吻合。
为什么默认拆成 4 个子质询?单个质询的求解时间服从指数分布,让它像抽奖:同样的难度,一位访客 30 毫秒解完,下一位要三秒。拆分把总难度d均摊到 4 个独立子质询上(源码见mintHashwxChallenges中的each = Math.max(1, Math.round(difficulty / count))),让求解时间更均匀。文档在 8 核 M3 上用正式版 Chrome 实测(每组 72 次求解,两种难度调到相同中位数):
| 1 个质询,d = 1,330,000 | 4 个子质询,d = 1,000,000 | |
|---|---|---|
| 中位数 | 536 ms | 490 ms |
| p90 | 1447 ms | 778 ms |
| 72 次中最慢 | 2378 ms | 1490 ms |
对给定的难度,拆分并不改变攻击者要付出的代价(预期工作量都是d次哈希),改变的是分布形状:单个质询的中位数只有均值的 0.69,四个子质询把中位数推到均值的 0.92 左右,长尾也随之缩短。验证组件自身的开销很小——worker 每 16 毫秒检查一次是否该停止(HASHWX_YIELD_MS = 16,见 widget/src/src/worker.js),这决定了子质询之间交接的最长时间。
浏览器引擎与移动端表现
HashWX 的 wasm 构建速度约为原生的 60%。三大引擎在单个 worker 上相差无几,所有核心跑满时 Safari 会落后(同一台 M3、同一次测试、各浏览器处于无头或前台):
| 引擎 | 1 个 worker | 8 个 worker | 解释执行回退 |
|---|---|---|---|
| Chrome 153 | 440 KH/s | 2050 KH/s | 94 KH/s |
| Firefox 156 | 420 KH/s | 1850 KH/s | 105 KH/s |
| Safari 27.2 | 410 KH/s | 1480 KH/s | 105 KH/s |
调难度时要以 Safari 为准:d = 1,000,000 时 Chrome 上平均约 0.5 秒,Safari 上约 0.7 秒。自行测量时务必使用各浏览器的正式发布版并让标签页保持在前台——Playwright 自带的 Firefox 跑任何 WebAssembly 负载都比正式版慢 3~6 倍,后台标签页则可能被调度到能效核心上让数字直接减半。
移动端(BrowserStack 真机,默认配置,每台 15 次求解,Vivo 30 次,每轮从刚加载的页面开始):
| 设备 | 系统 | 全部核心 | 中位数 | 最慢 |
|---|---|---|---|---|
| Galaxy S24 | Android 14 | 1238 KH/s | 1.1 秒 | 1.8 秒 |
| Pixel 9 | Android 15 | 837 KH/s | 1.4 秒 | 2.0 秒 |
| Pixel 6 | Android 12 | 746 KH/s | 1.9 秒 | 2.4 秒 |
| iPhone 15 | iOS 17 | 未测 | 1.9 秒 | 4.3 秒 |
| Redmi Note 11 | Android 11 | 456 KH/s | 2.4 秒 | 4.8 秒 |
| Vivo Y21 | Android 11 | 316 KH/s | 5.9 秒 | 11.0 秒 |
每次求解都包含两次到测试服务器的往返(各设备中位数 80~190 ms)。入门级 Android 手机的耗时约为 M3 台式机的十倍——如果你的流量以移动端为主,请调低难度;求解时间与难度成正比,500,000大约能让表中每个数字的求解部分减半。此外,没有 WebAssembly 的客户端根本解不了 HashWX(会直接拿到错误),需要支持它们时应改用有纯 JS 回退方案的 SHA-256 PoW(回退求解器实现见 widget/src/src/worker.js 的solveFallback);iPhone 上验证组件需要 iOS 15 或更高版本。
与 Instrumentation 质询协同:付出 + 环境
工作量证明证明的是付出(客户端必须消耗 CPU 周期寻找哈希),但它无法证明计算发生在真实浏览器里——攻击者可以用脚本直接跑求解器。这正是 Instrumentation 质询的用武之地:每次请求时服务端生成一段独一无二的 JavaScript 程序(core/src/instrumentation.js 的generateInstrumentation),在访问者浏览器中执行后把答案发回,由服务端对照预先并行跟踪的期望值校验,在接受令牌前确认对方处于真实浏览器环境。
它的核心机制值得展开:
- 主计算链:多个整数变量以随机种子值初始化,经过约 20 轮随机化操作不断变换,包括按位 AND/OR/XOR/NAND、原型链技巧(
fnHelper中的构造函数原型篡改),以及基于 DOM 的运算(domHelper向页面追加一棵元素树,沿树回溯累加数值,最后移除)。 - 为什么用 DOM 操作:纯算术运算在非浏览器环境里直接运行这段 JS 就能复现;DOM 操作做不到,至少无法低成本地做到——构建真实元素树、通过排版引擎读取数值、再拆除它们,触及的是非浏览器运行时常常只做桩实现、实现得不正确或出于性能跳过的那部分。质询因此很难在真实渲染引擎之外被重放。
- 自动化浏览器检测:可选开启
blockAutomatedBrowsers,脚本会运行 realm 逃逸检测与行为检测(headless Chromium 标记、webdriver 属性、window/document上的自动化框架注入标记等),识别 Playwright/Puppeteer/Selenium 等。不过文档如实说明:这些检查并非万无一失,即使是 Turnstile 这样的商业闭源 CAPTCHA,攻击者也能用打过补丁的隐身浏览器绕过。 - 执行环境:所有检查都在 iframe 内运行,通过
postMessage把答案发回父页面。服务端通过verifyInstrumentationResult校验返回的state是否与expectedVals完全一致(core/src/instrumentation.js)。
两者互补而非冗余:PoW 抬高"付出"维度的成本,Instrumentation 验证"环境"维度的真实性,在两个独立维度上抬高滥用成本。面对有决心的攻击者,单独任何一个都不够,但同时攻破两者就难得多。同样重要的是文档的告诫:Instrumentation 并非万无一失,不建议用它替代工作量证明——在没有 PoW 的情况下,攻击者用真实浏览器就能低成本地批量通过这类质询。
局限与边界:HashWX 防不住什么
与 RSW 防不住的是同一件事:定制芯片。专门为 HashWX 打造的 FPGA 或 ASIC 仍然会赢过 CPU。对刷 CAPTCHA 来说这笔账通常算不过来(ASIC 的一次性工程费用高达数百万美元),但这终究不是密码学层面的保证。
它同样拦不住人工打码农场,而且永远拦不住——工作量证明抬高的是每个请求的成本,不是人工的劳动。正确姿势是把 HashWX 与 instrumentation 质询搭配使用,让攻击者还必须提供真实的浏览器环境。
实战:在哪里配置难度与协议
cap-core:format-2 API 手动启用 HashWX
在 docs/zh/guide/capjs-core.md 中,cap-core 的默认仍是 SHA-256 PoW,HashWX 需要通过 format-2 API 手动启用:
import { generateChallenge, validateChallenge } from "capjs-core"; const SECRET = process.env.CAP_SECRET; app.post("/api/challenge", async () => { return await generateChallenge(SECRET, { format: 2, protocols: ["hashwx", "instrumentation"], hashwxDifficulty: 1_000_000, // 可选,这就是默认值 }); }); app.post("/api/redeem", async (req) => { return await validateChallenge(SECRET, req.body, { consumeNonce }); });关键参数(默认值与边界均来自 core/src/hashwx.js):
hashwxDifficulty:客户端预期计算的哈希次数,默认1_000_000,合法范围[1, 1_000_000_000];hashwxChallengeCount:拆分的子质询数,默认4,范围[1, 64];每多一个子质询,验证开销约增加 20 µs;hashwxNoncesPerHash:每个生成函数覆盖的 nonce 数,默认65_536,范围[1, 1_048_576]。
HashWX 不需要密钥材料,启动时也无任何准备;第一次验证会编译内置的 WebAssembly 模块(耗时几毫秒),可在启动阶段调用hashwxReady()把这份开销从首个请求挪走。验证组件会自动检测 format-2 响应,只需升级服务端即可。
Cap Standalone:按站点密钥切换协议
在 docs/zh/guide/standalone/options.md 中,HashWX 是新建密钥的默认协议,且按站点密钥配置:个别密钥可以用 SHA-256,其余继续留在 HashWX 上;在 HashWX 成为默认之前创建的密钥会保持原有设置直到手动更改。打开某个密钥的Configuration标签页,在Challenge protocol下选择协议即可,无需任何准备工作(没有密钥对要生成,也没有东西要持久化)。
难度由HashWX difficulty滑块控制(客户端预期计算的哈希次数),默认1_000_000,有效范围50_000–5_000_000,拆分为四个子质询。调高之前请先参考上文移动端测量结果。Instrumentation 质询在新建站点密钥时默认开启,可在密钥配置中开关;"Attempt to block headless browsers" 可阻止无头浏览器求解。较高的 instrumentation 混淆级别会显著降低生成吞吐量,除非需要更强混淆,建议保持在级别 3(obfuscationLevel默认值,1–10 可选,参见 docs/zh/guide/capjs-core.md 的 Instrumentation 一节)。
延伸阅读
- CAPTCHA 与转化率:质询摩擦对注册量的代价,以及低摩擦机制为何在转化率上更优
- 2026 年最佳 CAPTCHA 替代方案:与其他机制(Turnstile、hCaptcha、FriendlyCaptcha、ALTCHA 等)的横向对比
- HashWX 工作量证明:抗 GPU 协议的完整设计与测量细节
- Instrumentation 质询:第二层验证的机制与局限
- Core 无状态服务端库:
generateChallenge/validateChallenge的完整 API 与无状态部署模式(Cloudflare Workers、Bun) - Standalone 配置选项:环境变量、速率限制、Redis/Valkey、健康检查与 HashWX 滑块
- 网络安全
- 应用安全
- 后端
【免费下载链接】cap
Free, open-source and self-hosted CAPTCHA alternative to reCAPTCHA. Privacy-first and powered by proof-of-work and instrumentation challenges.
相关推荐
Cap 反机器人有效性解析:proof-of-work 与 instrumentation 如何让滥用变昂贵
Cap 反机器人有效性解析:proof of work 与 instrumentation 如何让滥用变昂贵 Cap 是一个免费、开源、可自托管的 CAPTCH
网络安全应用安全后端深入解析 Cap 工作原理:基于 SHA-256 工作量证明与 instrumentation 的自托管 CAPTCHA
深入解析 Cap 工作原理:基于 SHA 256 工作量证明与 instrumentation 的自托管 CAPTCHA Cap 是一个免费、开源、可自托管的
网络安全应用安全后端Cap 防机器人有效性解析:为什么 Proof-of-Work 与 Instrumentation 的组合能抬高滥用成本
Cap 防机器人有效性解析:为什么 Proof of Work 与 Instrumentation 的组合能抬高滥用成本 本篇指南围绕 Cap(一个免费、开源、
网络安全应用安全后端
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考