使用 Cap 保护你的表单
Cap 是一个开源的 CAPTCHA.
在前几次更新中, 原来用于保护评论功能的 Cloudflare Turnstile 已经被我用 Cap 替代了. 为什么要替代他呢, 其实也很简单, 因为在 某些地区 访问 cf 速度太慢, 就算点击了也会过不了验证. (其实这个验证我感觉也没什么必要, 只是看大伙好像都有, 我也不能落下)

至于其他的一些验证嘛, 比如说 Google 的 reCAPTCHA, 是我用过体验最差的, 没错, 就是让你选出红绿灯那个, 先不说别人用的怎么样, 反正我是要炎上了.
红绿灯的柱子算红绿灯吗?
这张图片里就漏了个角, 这张也算吗?
我感觉完全和上次一样啊, 为什么这次没过?
......
这个验证的反人类程度一度让我怀疑我到底是不是人类.
我想要的 CAPTCHA 应该是尽可能少的介入正常使用逻辑的 (比如说像 Turnstile 一样只需要点一下就够了), 并且拥有足够的隐私性的, 不然的话跟国内的某些厂商又有什么区别.
于是, 我就找到了 Cap 这个开源项目, 他不会像 reCAPTCHA 那样进行解谜验证, 而是使用一种叫做 proof-of-work (工作证明) 的机制, 来提高攻击的成本.
也就是说 Cap 并没有 bot 识别的能力, 只能拖慢 bot 的的速度而已.
因此, 要更高级别的防护的话, Cap 可能并不适合你.
好久没写博客了, 感觉已经不知道怎么写了 >.<
Proof-of-Work
Cap 使用的这种工作证明的机制其实就是让浏览器在提交表单之前进行暴力的计算 (比如说穷举某个特定的哈希值), 然后把计算得出的结果返回给服务端进行验证, 看是不是正确的结果. (不然随便发个都能过也没有验证的意义了)
在 Next.js 中使用
前排提示, 这里的内容写的比较复杂 (其实还是很简单的), 遇到看不懂的直接跳过即可.
因为我比较懒, 所以不想在重新配置 CSP (Content Security Policy, 内容安全策略) 了, 之前配置 CSP 可折磨了, 本来还想搭配 Next.js 中间件使用 nonce 的, 折腾了好久, 最后放弃了.
说起来 Next.js 的中间件也不是正经的中间件, 甚至都不再同一个上下文 (Next.js 的中间件使用 edge 运行时, 而服务端组件一般是 node 运行时), 顶多算是前置处理函数, 所以在 next.js16 中间件更名为 proxy (代理) 了.
扯远了, 因为不想动 CSP 了, 所以我打算直接由自己提供所需的 js 文件. 但是在官方文档中并没有提供现代化 Web 框架的使用示例, 但是没有关系, Cap 不是一个开源的项目嘛, 直接翻一下源码就知道该怎么用了.
首先, 根据文档上的说明, 我们需要先安装 @cap.js/widget 这个包, 看名字就知道, 这个包是在客户端上渲染的 CAPTCHA 交互控件. 先导入这个包, 并且在 React 组件中返回 <cap-widget /> HTML 标签, Cap 会自动找到这个标签并进行渲染.
但是我们还不知道怎么初始化, 先去看看源码. 发现导出的是一个 Cap 类, 这个类接收两个参数, 第一个参数是配置, 第二个参数是 <cap-widget /> HTML 标签, 如果不传这个参数的话就会创建一个新的 <cap-widget /> 并且藏起来. 这个藏起来的标签是没什么用的 (应该吧).
但是, 这不是重点, 因为我们可能并不需要用到这个 Cap 类, 为什么呢, 因为这份代码有这样的一部分:
;
这是 JS 中的 "立即调用函数表达式", 定义了一个函数之后立马使用 () 运行这个函数. 也就是说, 只要导入这个包, 这里的代码就会被运行. 也就是说只需要导入这个包, 他自己就会完成初始化, 并不需要我们手动添加初始化代码. 可以在 609 行找到相关的 HTML 元素定义.
那么代码也很简单了, 一行导入, 一行返回标签, 一个 React 组件就完成了.
"use client";
;
;
现在, 在页面中加入这个组件, 你应该就可以看到白色的验证框了.
还没完, 如果你配置了 CSP 的话, 这个组件应该还是用不了的, 因为 CSP 会拒绝来自 CDN 的另一个组件, @cap.js/wasm. 这是 rust 实现的计算模块, 为了加速计算的, 如果没有手动指定的话 Cap 组件会自己去公共 CDN 上拿.
既然组件都搬过来了, 那再把 wasm 模块一起搬过来吧.
这个 @cap.js/wasm 模块也可以像组件一样直接在客户端导入 (如果你没开 React 编译器功能的话, next.js 对 wasm 的兼容有点怪).
如果开了的话, 那就只能用土方法了. 把 wasm 复制到 publice 下, 让客户端自己来拿. 设置 CAP_CUSTOM_WASM_URL "环境变量" 就可以自定义获取 wasm 的 url 了. 类似于下面这样, Cap 就会自己去 publice/cap/cap_wasm.js 拿.
"use client";
;
;
如何获得验证产生的 Token 呢, 可以按照官方提供的方法, 直接使用 DOM 操作拿到组件对应的元素, 然后使用 addEventListener 来获得 token, 或者用更加符合 React 风格的事件处理函数.
在组件的源码中可以看到 相关的定义, 在验证完成后会尝试运行 onsolve 这个属性的字符串代码.
但是在使用 React 传入一个自定义的处理函数的时候有点不同, 在使用 onXXX 这类属性的时候会直接给 XXX 添加一个监听器, 比如使用 onClick 的时候其实是为某个元素执行了类似这样的代码:
;
所以给 onsolve 添加函数的时候 React 会直接往 <cap-widget /> 的 solve 上注册一个监听事件, 而 <cap-widget /> 的 solve 恰好是处理验证逻辑的函数, 所以在 solve 之后 React 的处理函数就会被执行. (竟然没有 bug, 真神奇. 别管是怎么实现的, 你就说他你不能跑吧, 说不定作者就是这样设计的呢)
至于触发器是如何拿到参数 e 的呢, 在验证跑完之后 Line236 调用 this.dispatchEvent(), 然后 this.dispatchEvent 又会调用 EventTarget.dispatchEvent 方法, 来派发事件. 事件派发之后所有监听 solve 的触发器都会被触发, 并且拿到在 this.dispatchEvent 中构造的数据.
于是我们就可以这样优雅的使用函数来处理 Token:
"use client";
;
;
或者你可以直接把组件放进表单, 组件里自带一个 cap-token 的 input, 提交表单的时候会一起自动提交上去.
好了, 现在组件的问题全部解决了, 但是如何在 CSP 的限制下与 Cap 服务器交互呢? 其实也不难, 既然他拦截跨源请求, 那直接把他变成同源的不就行了.
创建一个专门用于代理 Cap 请求的 API 端点, 具体的话, 可以参考 Next.js 文档 中的示例, 可能需要注意一下双重 Gzip 压缩的问题.
至于服务端嘛, 就暂时先使用官方提供的 docker 镜像吧, 不过我好像看到了 django 集成, 等有时间在去看看. 诶嘿, 又可以水一篇, "分割商法"这一块.
最近因为崩铁出了个新角色, 又被别人骗去玩了崩铁. 虽说是崩坏系列, 但是除了名字里其他地方也找不到 "崩坏" 了 (在前作中 "崩坏" 是一种灾害, 第二次崩坏 (漫画))
说起来还看到个 ElysiaJS, 爱莉希雅 JS, 这下不得不用了.

2025-11-9: 我在写什么, 干脆删掉算了