顶呱呱的快乐
顶呱呱的快乐
发布于 2026-07-19 / 21 阅读
0
0

Cap-Pow 部署与使用教程:给网站接入无图片 PoW 人机验证

Cap-Pow 工作量证明人机验证教程封面

本文重点使用已经上线的实例 https://cap-pow.wuw.li/。需要先说明:这个实例采用的是 onexru/cap-pow-php-server 的接口形态并配有定制前端脚本;它与当前 Cap 官方 Standalone(Docker + Valkey + 站点密钥)不是同一套部署和调用方式,不能把两套示例直接混用。

传统验证码经常要求选图片、拖动滑块,既打断操作,也可能引入第三方跟踪。Cap/Cap-Pow 的思路是让浏览器完成一小段工作量证明(Proof of Work,PoW):服务端生成挑战,浏览器计算答案,服务端核验后签发一次性 token,业务后端再消费这个 token。

本文分为三部分:

  1. 直接使用已经部署好的 cap-pow.wuw.li
  2. 自建与该实例相同接口的 PHP 服务;
  3. 了解当前官方 Cap Standalone 的 Docker 方案及其差异。

一、先理解完整验证链路

以现有实例为例,流程不是“前端显示一个绿色勾就算通过”,而是:

  1. 浏览器向 POST /challenge 获取挑战;
  2. 浏览器计算 SHA-256 PoW;
  3. 浏览器将挑战 token 和答案提交到 POST /redeem
  4. 服务端返回验证 token;
  5. 页面把验证 token 与表单数据一起交给业务后端;
  6. 业务后端调用 POST /api/validate
  7. 只有响应中的 successtrue,业务后端才继续处理登录、注册、留言或表单提交。

cap-pow.wuw.li 首页列出的三个接口正是 /challenge/redeem/api/validate。实际页面脚本也实现了上述流程。

安全边界一定要记住:前端回调只能拿到 token,真正决定“放行”的步骤必须在业务后端。 如果只在 JavaScript 中判断,攻击者可以绕开页面直接请求业务接口。


二、直接使用已部署实例

1. 引入现有实例的脚本和样式

页面提供了可跨域加载的 JS 与 CSS:

<link rel="stylesheet" href="https://cap-pow.wuw.li/cap-pow.css">
<script src="https://cap-pow.wuw.li/cap-pow.js"></script>

当前脚本为零依赖实现,内部固定使用 https://cap-pow.wuw.li 作为 API 地址;计算依赖浏览器的 TextEncodercrypto.subtle.digest()fetch()

为了避免只复制首页中带省略号的演示代码,可使用下面这份与现有 CSS/JS 类名匹配的完整组件:

<div class="cap-wrap">
  <div class="captcha">
    <div
      class="cap-ct"
      id="cap-ct"
      role="button"
      tabindex="0"
      aria-label="点击进行人机验证"
      onclick="CapPow.go()"
    >
      <div class="cap-cb">
        <div class="cap-check">
          <svg viewBox="0 0 24 24" aria-hidden="true">
            <polyline points="4,12 9,17 20,6"></polyline>
          </svg>
        </div>
        <svg class="cap-ring" viewBox="0 0 32 32" aria-hidden="true">
          <circle class="cap-ring-bg" cx="16" cy="16" r="14"></circle>
          <circle class="cap-ring-fg" cx="16" cy="16" r="14"></circle>
        </svg>
      </div>
      <div class="cap-lw">
        <span class="cap-label active">验证你是人类</span>
      </div>
    </div>
    <a class="cap-credits" href="https://github.com/tiagozip/cap" target="_blank" rel="noopener">Cap</a>
  </div>
</div>

脚本会给可点击元素补充 Enter/空格键支持。成功状态约 30 秒后复位;失败状态约 4 秒后复位。这只是组件显示状态的时间,不等于后端 token 的有效期。

2. 接收 token

推荐在表单里准备一个隐藏字段,并通过回调写入:

<form id="protected-form" method="post" action="/your-submit-endpoint">
  <!-- 你的其他字段 -->
  <input type="hidden" name="cap_token" id="cap-token">

  <!-- 把上一节的 Cap-Pow 组件放在这里 -->

  <button type="submit">提交</button>
</form>

<script>
  CapPow.onDone = function (token) {
    document.getElementById('cap-token').value = token;
  };

  CapPow.onFail = function (message) {
    document.getElementById('cap-token').value = '';
    console.warn('Cap-Pow 验证失败:', message);
  };
</script>

也可以监听脚本发出的 cap-solve 自定义事件:

document.getElementById('cap-ct').addEventListener('cap-solve', function (event) {
  console.log(event.detail.token);
});

注意:当前 CapPow.onDoneCapPow.onFail 是全局单一回调。如果同一页放多个受保护表单,需要自行按元素管理状态,不能假设每个组件都拥有独立回调。

3. 在业务后端消费 token

现有实例首页给出了 PHP 核验思路。下面增加了超时、网络失败和 JSON 格式检查:

<?php
$capToken = $_POST['cap_token'] ?? '';
if ($capToken === '') {
    http_response_code(400);
    exit('请先完成人机验证');
}

$ch = curl_init('https://cap-pow.wuw.li/api/validate');
curl_setopt_array($ch, [
    CURLOPT_POST => true,
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_HTTPHEADER => ['Content-Type: application/json'],
    CURLOPT_POSTFIELDS => json_encode(['token' => $capToken]),
    CURLOPT_CONNECTTIMEOUT => 3,
    CURLOPT_TIMEOUT => 8,
]);

$raw = curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
$error = curl_error($ch);
curl_close($ch);

$result = is_string($raw) ? json_decode($raw, true) : null;
if ($error !== '' || $status !== 200 || !is_array($result) || ($result['success'] ?? false) !== true) {
    http_response_code(403);
    exit('人机验证失败或已过期,请重试');
}

// 只有到这里,才继续执行真正的登录、注册、留言或提交逻辑。

核验时应采用 fail closed:验证服务超时、返回非 200、JSON 无法解析或 success 不为 true 时一律拒绝,而不是跳过验证码。

仓库源码显示,PHP 版本的验证 token 默认有效期为 20 分钟,并在成功验证后立即删除。因此 token 是一次性的:重复提交、浏览器后退后再次提交、前端重试复用旧 token 都会失败。本文实测现有实例也符合这一行为:第一次核验成功,第二次使用同一 token 返回失败。

4. 接口调试

只测试挑战获取可执行:

curl -sS 'https://cap-pow.wuw.li/challenge' \
  -X POST \
  -H 'Content-Type: application/json' \
  -d '{}'

返回结构类似:

{
  "challenge": { "c": 8, "s": 64, "d": 3 },
  "token": "……",
  "expires": 1784419034000
}

这里的数值是核实时现有实例返回的配置,不应当视为项目永久默认值。上游 PHP 仓库默认值是 c=64s=128d=4,部署者可以修改。

不要用假的 token 判断整个流程是否可用;完整成功路径必须先完成 challenge 与 redeem,最后再 validate。


三、在 Halo 中怎么用

场景 A:把组件放进 Halo 文章做演示

如果只是写一篇演示文章,可以把组件 HTML 放入支持原始 HTML 的内容位置,并确保页面最终加载 CSS 与 JS。具体脚本放置方式取决于正在使用的 Halo 编辑器、主题及其安全策略,应以页面最终源码和浏览器控制台为准。

但这只是一段演示 UI,不会自动保护 Halo 的评论、登录、注册或插件表单

场景 B:真正保护 Halo 表单

要保护 Halo 中的某个操作,至少要同时完成两端改造:

  • 前端:在对应主题模板或插件 UI 中渲染组件,把 token 随请求提交;
  • 服务端:在执行真实业务之前调用 /api/validate,验证失败就拒绝请求。

只编辑文章内容无法给 Halo 服务端新增 token 核验逻辑。评论、登录或注册等核心流程应通过合适的 Halo 插件/扩展点实现;不要仅靠隐藏提交按钮或前端 JavaScript。

如果你修改主题,应避免直接改已安装主题的成品文件后长期运行,因为主题更新可能覆盖改动。更稳妥的做法是维护自己的主题派生版本,或把功能封装成插件。

场景 C:Halo 只是发布这篇教程

Halo 控制台支持“保存但不发布”和未发布预览。因此本文可先作为草稿导入/粘贴,完成代码块、主题样式和移动端预览检查后再决定是否发布。


四、自建与现有实例同源的 PHP 版本

对应仓库为:

https://github.com/onexru/cap-pow-php-server

1. 已核实的环境要求

仓库 README 明确列出:

  • 推荐 Nginx;
  • 推荐 PHP 8.0+;
  • 使用 SQLite。

源码还表明运行环境需要:

  • PHP 的 PDO SQLite 驱动;
  • Web/PHP 运行用户对数据库目录具有写权限;
  • random_bytes()、SHA-256 等 PHP 标准能力。

仓库只有 cap.phpchallenge.phpredeem.phpvalidate.php、演示首页等文件,没有官方 Dockerfile 或 docker-compose 文件。因此不要把互联网上未经核对的 PHP 镜像编排当作该仓库的官方 Docker 安装方式。

2. 安装步骤

下载源码后,将文件部署到站点目录。仓库提供的 Nginx 规则为:

location / {
    try_files $uri $uri/ $uri.php?$query_string;
    if ($request_filename ~* .*\.php$) {
        return 403;
    }
}

这条规则的目的,是把 /challenge 映射到 challenge.php,同时阻止用户直接访问以 .php 结尾的 URL。它需要合并进你现有站点配置,不能不看上下文就覆盖完整的 Nginx server 块;PHP-FPM 的实际转发配置仍应沿用服务器现有、已经可用的配置。

默认数据库路径在 cap.php 中为:

private $db_Driver = '.data/cap.db';

首次运行时程序会尝试创建目录、SQLite 数据库、challenges 表、tokens 表和索引。部署完成后应检查:

php -m | grep -Ei 'pdo|sqlite'

并确认 PHP-FPM 用户能够写入 .data/。数据库文件不应被 Web 服务器作为静态文件下载;生产环境更稳妥的做法是把数据库放到站点公开目录之外,再通过构造配置指定绝对路径。

3. 难度与超时配置

仓库默认配置:

$config = [
    'db_Driver' => '.data/cap.db',
    'c' => 64,
    's' => 128,
    'd' => 4,
];
$cap = new Cap($config);

含义按项目命名分别为运算次数 c、每次运算输入长度 s 和难度 d。生成挑战时还能设置挑战参数及过期秒数:

$cap = new Cap();
$challenge = [
    'c' => 32,
    's' => 64,
    'd' => 4,
];
$expires = 60;
$cap->createChallenge($challenge, $expires);

难度越高,客户端计算时间通常越长。不要直接照抄某个实例的值上线,应在手机、低性能电脑和主流浏览器上测试等待时间与失败率。

4. 自建后的前端地址

PHP 仓库 README 使用官方 Cap Widget:

<script src="https://cdn.jsdelivr.net/npm/@cap.js/widget"></script>
<cap-widget
  id="cap"
  data-cap-api-endpoint="https://你的-Cap-Pow-地址/"
></cap-widget>

不过 cap-pow.wuw.li 当前公开的是自己的 cap-pow.js/cap-pow.css,并非 README 里的原始组件。若想复刻现有实例外观,需要同时部署或改写对应的前端资源,并把脚本中的 API 常量改为自己的域名。不要把当前实例脚本原样放到自己的域名后就认为已经自托管——其公开 JS 内部仍固定指向 https://cap-pow.wuw.li

5. PHP 分支的安全注意

仓库 cap.php 文件头明确写着该版本“不带防重放攻击”,并推荐需要该能力时使用作者的 One-Pow 分支。与此同时,当前仓库代码确实会把 challenge 和最终验证 token 设为一次性并在消费后删除;这里应忠实保留项目作者的风险声明,不把它宣传成可替代所有反滥用措施的完整方案。

生产环境还应在反向代理或应用层补充:

  • 对 challenge、redeem、validate 做合理限流;
  • 只允许 HTTPS;
  • 限制允许调用挑战接口的来源;
  • 监控异常请求和失败率;
  • 定期清理过期数据(当前源码只在访问对应 token 时删除过期记录,未见全表定时清理逻辑);
  • 保护 SQLite 文件与备份。

最后一条“过期数据清理”来自源码检查:建表并写入 expires,但公开仓库中没有独立清理任务。因此高流量部署前应自行评估数据库增长,而不是假设项目已提供后台清理器。


五、Docker:当前官方 Cap Standalone 是另一条路线

如果你的目标不是一比一复刻 PHP 实例,而是部署当前 Cap 官方推荐版本,官方文档推荐 Docker。它运行于 Bun,使用 Redis/Valkey,带管理面板、多个 site key、统计、instrumentation challenge,以及兼容 reCAPTCHA 风格的服务端核验 API。

官方当前提供的 Compose 核心配置如下:

services:
  cap:
    image: tiago2/cap:latest
    container_name: cap
    ports:
      - "3000:3000"
    environment:
      ADMIN_KEY: your_secret_password
      REDIS_URL: redis://valkey:6379
    depends_on:
      valkey:
        condition: service_healthy
    restart: unless-stopped

  valkey:
    image: valkey/valkey:9-alpine
    container_name: cap-valkey
    volumes:
      - valkey-data:/data
    command: valkey-server --save 60 1 --loglevel warning --maxmemory-policy noeviction
    healthcheck:
      test: ["CMD", "valkey-cli", "ping"]
      interval: 5s
      timeout: 3s
      retries: 5
    restart: unless-stopped

volumes:
  valkey-data:

启动:

docker compose up -d

然后访问 http://服务器地址:3000,用 ADMIN_KEY 登录并创建 site key。官方建议 ADMIN_KEY 至少 32 个字符。

官方 Standalone 的前端 endpoint 包含 site key:

<script type="module" src="https://cdn.jsdelivr.net/npm/cap-widget"></script>

<form>
  <cap-widget
    required
    data-cap-api-endpoint="https://cap.example.com/你的-site-key/"
  ></cap-widget>
  <button type="submit">提交</button>
</form>

组件位于表单中时,会自动注入默认名为 cap-token 的隐藏字段。业务后端应调用:

curl 'https://cap.example.com/你的-site-key/siteverify' \
  -X POST \
  -H 'Content-Type: application/json' \
  -d '{"secret":"你的-site-secret","response":"浏览器返回的-token"}'

成功响应为:

{ "success": true }

请区分三种密钥/值:

  • ADMIN_KEY:登录 Standalone 管理面板;
  • site key:公开给浏览器,用在 endpoint 中;
  • site secret:只保存在业务后端,用于 /siteverify

它们与 PHP 实例的 /api/validate 无密钥调用方式不同。

Standalone 常用环境变量

官方文档中与常见部署最相关的选项包括:

  • CORS_ORIGIN:默认 *,可用逗号分隔多个来源;
  • REDIS_URL:Redis/Valkey 连接地址;
  • REDIS_PREFIX:共享 Redis 时给键增加命名空间;
  • RATELIMIT_IP_HEADER:反向代理后指定真实 IP 请求头,Cloudflare 可用 cf-connecting-ip,多数代理环境常用 x-forwarded-for
  • ENABLE_ASSETS_SERVER=true:由实例提供 widget/WASM 静态资源;
  • WIDGET_VERSIONWASM_VERSION:固定资源版本。官方不建议生产环境使用可能引入破坏性变更的 latest
  • SHOW_ERRORS=true:取消错误信息脱敏,仅适合有明确需求的调试环境;
  • DISABLE_ERROR_LOGGING=true:关闭错误日志。

如果为 IP 数据库绑定宿主目录到 /usr/src/app/data,官方文档说明容器以 UID 1000 运行,该目录必须对 UID 1000 可写。无法调整所有权时,可改用 Docker named volume 或不做宿主目录绑定。


六、常见问题

1. 点击后提示“网络错误”或“提交失败”

依次检查:

  • 页面是否为 HTTPS;
  • cap-pow.jscap-pow.css 是否加载成功;
  • 浏览器开发者工具 Network 中 /challenge/redeem 是否返回 200;
  • CSP 是否阻止外部脚本、样式、connect-src 或 WebAssembly;
  • 浏览器是否支持 crypto.subtle
  • 自建服务的跨域配置是否允许当前站点。

现有 cap-pow.wuw.li 的 JS、CSS 和 API 响应目前都带有 Access-Control-Allow-Origin: *,但自建实例不能据此假设自己的 Nginx/PHP 配置也会自动具有相同响应头。

2. 前端已显示“你是人类”,后端仍失败

常见原因:

  • 提交的字段名与后端读取字段名不一致;
  • token 已被消费一次;
  • token 过期;
  • 页面刷新或组件重置后仍提交旧 token;
  • 后端调用了错误的验证接口;
  • 混用了 PHP 实例的 /api/validate 与 Standalone 的 /<site-key>/siteverify

3. 页面能显示组件,但业务接口仍可被绕过

说明只做了前端,没有在业务后端强制核验 token。前端组件不是访问控制边界。

4. SQLite 报错 unable to open database file

重点检查数据库父目录是否存在、PHP-FPM 用户是否有写权限、路径是否按运行目录解析。建议改为明确的绝对路径,并确保数据库不在可下载的公开静态目录内。

5. Nginx 访问 /challenge 返回 404

检查仓库提供的 try_files $uri $uri/ $uri.php?$query_string; 是否合并到正确的 server/location,以及 PHP-FPM 转发规则是否实际生效。不要为了修复单个路由直接覆盖整份站点配置。

6. 手机验证很慢

PoW 会消耗客户端 CPU。先降低挑战参数并进行真机测试;不要只在高性能桌面电脑上评估。官方 Standalone 还提供按 site key 配置的挑战方式,但那属于 Standalone 路线,不能直接套入 PHP 分支。

7. 能否完全离线、不依赖第三方 CDN?

  • 当前实例方案:如果页面直接加载 cap-pow.wuw.li 的 JS/CSS 并调用它的 API,就仍然依赖第三方实例;
  • PHP 自建:应把前端文件一并托管到自己的域名并确认 API 地址已改成自己的实例;
  • 官方 Standalone:可开启 asset server,自行提供 widget 与 WASM,并固定版本。

“上游项目隐私友好”不等于“调用任意第三方实例时没有数据出站”。访问第三方验证域名时,至少会发生网络请求,部署者应结合隐私政策、可用性和日志策略自行评估。


七、上线检查清单

  • 明确选择 PHP 分支或官方 Standalone,没有混用接口;
  • 页面端能完成 challenge → redeem;
  • 业务后端强制执行 validate/siteverify;
  • 验证服务故障时默认拒绝,而不是绕过;
  • token 验证成功后不重复使用;
  • 全站 HTTPS;
  • CORS 只放行所需站点(自建时);
  • 反向代理传递并正确解析客户端 IP;
  • challenge/redeem 等接口有限流;
  • 移动设备和低性能设备完成过验证耗时测试;
  • SQLite 或 Valkey 数据有持久化与备份策略;
  • 前端资源固定版本或纳入自己的发布流程;
  • Halo 主题/插件升级后重新做完整流程回归测试。

参考资料

  1. 已部署实例与接入示例:https://cap-pow.wuw.li/
  2. 已部署实例 JavaScript:https://cap-pow.wuw.li/cap-pow.js
  3. 已部署实例 CSS:https://cap-pow.wuw.li/cap-pow.css
  4. Cap-Pow PHP Server 仓库:https://github.com/onexru/cap-pow-php-server
  5. PHP Server README:https://github.com/onexru/cap-pow-php-server/blob/main/README.md
  6. PHP 核心实现 cap.phphttps://github.com/onexru/cap-pow-php-server/blob/main/cap.php
  7. Cap 官方仓库:https://github.com/tiagozip/cap
  8. Cap Standalone 官方文档:https://trycap.dev/guide/standalone/
  9. Cap Standalone 配置选项:https://trycap.dev/guide/standalone/options
  10. Cap Widget 官方文档:https://trycap.dev/guide/widget
  11. Cap 官方 Compose 文件:https://github.com/tiagozip/cap/blob/main/standalone/docker-compose.yml
  12. Halo 文章管理文档:https://docs.halo.run/user-guide/posts

版本提示:Cap 上游仍在持续更新,正式部署前应重新核对镜像标签、Widget 版本、环境变量和接口文档;生产环境不应无审查地追随 latest


评论