☰
Cap Standalone 配置选项完全指南:CORS、Asset Server、限流、健康检查与 PoW 协议调优
2026/9/29 8:56:45 网站建设 项目流程
  • 网络安全
  • 应用安全
  • 后端

【免费下载链接】cap

Free, 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 Standalone 的 配置选项文档 为核心,系统讲解自托管部署该开源 CAPTCHA 服务时需要掌握的全部配置项:从 CORS 跨域策略、静态资源(Asset server)托管,到基于 IP 的请求限流、Redis/Valkey 存储、健康检查与优雅关闭,再到 HashWX / SHA-256 / RSW 三种证明工作量(PoW)协议与 instrumentation 挑战的逐键配置,最后涵盖 IP 地理数据库的接入与 Docker 卷权限问题。读完本文,你将能够为生产环境正确设置环境变量、锁定组件版本、排查资源缓存与限流故障,并为每个 site key 制定合适的挑战协议与难度。

本文所有结论均以本仓库 standalone/ 目录下的实际源码与 官方文档 为依据;文中给出的默认值、取值范围与环境变量名均与当前仓库代码一致。


一、CORS:控制谁可以发起与交换 challenge

Cap Standalone 默认允许任意来源(origin)的网页请求挑战并完成挑战交换,这一默认策略可以通过环境变量CORS_ORIGIN在启动服务时覆盖。

  • 默认值:*,即允许所有 origin。
  • 多个 origin:使用英文逗号分隔,例如domain1.tld,domain2.tld,...。

在源码层面,CORS_ORIGIN的解析逻辑位于 settings-cache.js:当值为*或未设置时,解析结果为{ origins: null }(等价于全部放行);否则会先按逗号切分、去除空白与*条目,得到一个白名单数组。该默认值会在服务启动时通过loadCorsDefault()载入内存缓存,见 index.js。

CORS 的最终判定在 index.js 的@elysiajs/cors插件中完成:

  • /assets及其子路径始终放行(资源需要被任意站点加载);
  • 其余路径交由checkCorsOrigin(request)判定,其实现位于 settings-cache.js:
    • 优先使用每个 site key 的corsOrigins配置(可在 APIPUT /server/keys/:siteKey/config中按键设置,见 server.js);
    • 未按键配置时回退到全局默认(即CORS_ORIGIN解析结果);
    • 判定时既支持完整的Origin头字符串精确匹配,也支持仅传host的宽松匹配;
    • 判定结果在内存中缓存 60 秒(CORS_CACHE_TTL),修改配置后通过invalidateCorsCache立即失效。

从源码结构看,这套机制同时支持"全局白名单 + 按键白名单"两层覆盖,适合多站点共用一台 Cap 的场景。


二、Asset Server:自托管 widget 与 WASM 静态资源

Cap Standalone 内置一个静态资源服务(Asset server),用于让前端页面不再依赖第三方 CDN 加载 widget 脚本与 WASM 二进制文件,从而满足隐私优先、自托管的要求。

2.1 开启与版本锁定

Asset server默认关闭,需要显式设置环境变量开启:

ENABLE_ASSETS_SERVER=true WIDGET_VERSION=0.1.58 WASM_VERSION=0.0.8
  • ENABLE_ASSETS_SERVER:置为true后,资源将从/assets端点对外提供;
  • WIDGET_VERSION与WASM_VERSION:必须与你希望托管的 widget / WASM 文件版本一致,可用的版本号即 npm 上@cap.js/widget与@cap.js/wasm的发布版本;
  • 两者默认值均为latest,会提供最新版本,但不建议在生产环境使用——小版本更新可能带来破坏性变更,导致线上系统突然不可用。

对应源码实现见 assets.js:只要ENABLE_ASSETS_SERVER=true且任一版本为latest,启动时就会打印警告日志,提醒生产环境应固定版本号。

2.2 资源路径与前端接入方式

开启后,文件会从以下路径对外提供(对应路由实现在 assets.js):

  • /assets/widget.js—— 标准 widget 脚本
  • /assets/floating.js—— 浮动模式(floating)widget 脚本
  • /assets/cap_wasm_bg.wasm—— 主 WASM 二进制
  • /assets/hashwx.wasm—— HashWX 专用 WASM
  • /assets/cap_wasm.js—— WASM 加载器(loader)

前端接入时,把<script>的src指向你服务器的对应路径即可,例如:

<script src="https://<server url>/assets/widget.js"></script>

浮动(floating)模式则使用:

<script src="https://<server url>/assets/floating.js"></script>

同时,将window.CAP_CUSTOM_WASM_URL与window.CAP_CUSTOM_HASHWX_URL分别指向cap_wasm_bg.wasm与hashwx.wasm,让 widget 从你的服务器加载 WASM 而不是第三方 CDN:

window.CAP_CUSTOM_WASM_URL = "https://<server url>/assets/cap_wasm_bg.wasm"; window.CAP_CUSTOM_HASHWX_URL = "https://<server url>/assets/hashwx.wasm";

需要特别注意一个版本边界:hashwx.wasm从@cap.js/wasm0.0.8 起才存在。如果WASM_VERSION指向更早的版本,/assets/hashwx.wasm会返回503。此时不要设置CAP_CUSTOM_HASHWX_URL,widget 会自动改从 jsdelivr 加载该文件(这一回退逻辑同样体现在 assets.js:当拉取hashwx.wasm失败时会打印警告并删除缓存键)。

2.3 资源来源:CACHE_HOST

默认情况下,上述文件在服务启动时从process.env.CACHE_HOST拉取,该变量默认值为https://cdn.jsdelivr.net,可在启动服务时通过设置CACHE_HOST替换为任意可访问的镜像或自建源:

  • 对应实现见 assets.js,资源 URL 形如${CACHE_HOST}/npm/@cap.js/widget@${WIDGET_VERSION}与${CACHE_HOST}/npm/@cap.js/wasm@${WASM_VERSION}/browser/...;
  • 文件被下载后写入 Redis(键名如asset:widget.js、asset:cap_wasm_bg.wasm等),随后由/assets/*路由从 Redis 读出并提供给客户端;
  • 资源路由还会附加Cache-Control: max-age=31536000, immutable响应头(assets.js),便于浏览器长期缓存。

2.4 常见故障排查

若访问某个资源端点得到Asset not cached yet的响应,说明文件尚未成功下载到 Redis 缓存(该响应对应路由内缓存缺失时返回的503,见 assets.js)。请按以下顺序检查:

  1. 确认容器内真的设置了ENABLE_ASSETS_SERVER=true:如果你在 compose 文件中修改了该值,需要重建容器使其生效;否则/assets/*会返回404,并附带说明 "Asset server is disabled"(见 assets.js)。
  2. 确认容器可以访问CACHE_HOST:若下载失败,启动时日志中会打印包含[asset server] failed to update assets cache的行,之后每1 小时重试一次(setInterval(updateCache, 1000 * 60 * 60),见 assets.js)。
  3. 确认WIDGET_VERSION与WASM_VERSION指向 npm 上真实存在的版本:版本号不存在时拉取必然失败,同上记录错误日志。

补充一点缓存刷新机制:updateCache会读取 Redis 中的asset:cache-config,只有距上次更新超过 1 天或WIDGET_VERSION/WASM_VERSION发生变化时才重新拉取(assets.js),因此锁定版本后资源缓存是长期稳定的。


三、请求限流:按客户端 IP 的固定窗口

Challenge 相关端点按客户端 IP 进行限流,采用固定时间窗口(fixed window)算法。

3.1 默认值与调整方式

  • 默认限制:每个 IP 每 5 秒最多 30 次请求;
  • 全局调整:在仪表盘Settings页面修改,或通过 APIPUT /settings/ratelimit设置;
  • 按键覆盖:在某个 site key 的Configuration标签页中可为该键单独设置ratelimitMax与ratelimitDuration;
  • 超限响应:返回 HTTP429,并带响应头X-RateLimit-Remaining: 0。

以上默认值(max: 30, duration: 5000)与 API 校验范围(max1–10000、duration1000–3600000 毫秒)在 server.js 中有完整定义。限流核心实现在 ratelimit.js:

  • 窗口键形如rl:{scope}:{ip}:{windowMs}:{window},通过 RedisINCR计数、首次计数时EXPIRE设置窗口过期;
  • 响应头同时包含X-RateLimit-Limit与X-RateLimit-Remaining;
  • 超过max时返回429与{ error: "Rate limit exceeded" };
  • 支持getLimits回调,实现按 site key 读取各自的限流参数(对应"Configuration 标签页按键覆盖")。

3.2 siteverify 端点不受限流

/siteverify端点专为服务器到服务器(server-to-server)的验证设计,默认不参与限流。对应的端点实现见 siteverify.js,它接收secret与response参数,校验密钥与一次性 token 后返回{ success: true }。

3.3 代理背后的客户端 IP 识别

Standalone 识别客户端 IP 时依次检查以下请求头:

  1. X-Forwarded-For
  2. X-Real-IP
  3. CF-Connecting-IP

全部缺失时才回退到 socket 层地址。这一顺序在 ratelimit.js 的DEFAULT_IP_HEADERS常量中原样体现(实现还处理了逗号分隔的链式值,取第一个条目)。

如果你位于使用其他请求头的反向代理之后,有两种方式指定 IP 来源:

  • 设置环境变量RATELIMIT_IP_HEADER(例如位于 Cloudflare 之后时可设为cf-connecting-ip);
  • 或在仪表盘Settings > Headers中配置 IP 头。

以 nginx 为例,务必确保代理把真实客户端 IP 传给 Cap:

location / { proxy_pass http://localhost:3000; proxy_set_header X-Forwarded-For $remote_addr; }

两个必须警惕的后果:

  1. 如果不转发真实 IP,所有请求都会被视为来自代理自身 IP,所有客户端将共享同一个限流配额桶,等于限流失效;
  2. X-Forwarded-For等头部按原样被信任。因此服务器绝不能直接暴露在公网,否则客户端可以伪造头部绕过限流(代码逻辑见 ratelimit.js,它无条件信任这些头)。

四、Redis / Valkey:全部持久化依赖

Cap Standalone 的所有持久化数据(site key、配置、会话、令牌、指标、资产缓存等)都存储在 Redis(或其兼容替代品 Valkey)中。

  • 连接配置:设置环境变量REDIS_URL为你的 Redis 连接字符串,默认值为redis://localhost:6379;
  • Valkey 推荐:官方快速开始指南推荐通过 docker-compose 使用 Valkey(Redis 兼容存储);
  • 多实例隔离:如果多个 Cap 实例(或其他应用)共用同一个 Redis 实例,应设置REDIS_PREFIX为所有键添加命名空间前缀。例如REDIS_PREFIX=cap:后,会话键存储为cap:session:...、指标键存储为cap:metrics:...。默认值为空字符串,因此已有部署不会受到影响。

对应源码见 db.js:连接 URL 实际是REDIS_URL || VALKEY_URL || redis://localhost:6379的三级回退;REDIS_PREFIX通过一个 Proxy 包装所有 Redis 命令,自动为get/set/hget/hmset/sadd/incr/expire等键名前置前缀(KEY_FIRST/KEY_ALL两组命令集合,见 db.js),读取KEYS结果时还会反向剥离前缀。

此外 db.js 还实现了连接中断自动重连:检测到连接类错误码时,以指数退避(500ms 起、上限 15s)重新建立连接,并对重连前的操作进行一次性重试,增强了自托管场景下的可用性。


五、健康检查与优雅关闭

Cap Standalone 提供两个无需认证的端点,供容器编排系统与监控告警使用。

5.1 /health 与 /health/live

  • GET /health:当 Redis 在2 秒内响应PING时返回200 {"status":"ok"},否则返回503 {"status":"unavailable"}。用于 readiness check(就绪探针)与告警;
  • GET /health/live:只要进程存活就返回200,即使 Redis 已宕机。用于 liveness check(存活探针),避免 Redis 故障期间编排系统反复重启 Cap。

同秒内到达的健康检查会复用同一次 PING,因此频繁调用/health不会给 Redis 增加额外负担。源码实现在 health.js:REDIS_TIMEOUT_MS = 2000对应 2 秒超时,REDIS_CHECK_REUSE_MS = 1000对应 1 秒内的结果复用;/health/live是无条件返回ok的纯静态端点。

5.2 Kubernetes 探针示例

readinessProbe: httpGet: path: /health port: 3000 livenessProbe: httpGet: path: /health/live port: 3000

5.3 Docker Compose 健康检查

若使用 Docker Compose,可在cap服务中追加以下配置。注意纯 Docker 模式只会把容器标记为 unhealthy,不会自动重启:

healthcheck: test: ["CMD", "bun", "-e", "fetch('http://127.0.0.1:3000/health').then((r) => process.exit(r.ok ? 0 : 1)).catch(() => process.exit(1))"] interval: 30s timeout: 5s retries: 3

5.4 SIGTERM / SIGINT 优雅关闭行为

收到SIGTERM或SIGINT时,Cap 会:

  1. 停止接受新连接;
  2. 等待正在处理的请求完成;
  3. 关闭 Redis 连接;
  4. 以退出码0正常结束。

若 8 秒后仍有请求在运行,则立即以退出码1结束——因此总能落在 Docker 默认 10 秒停止时限之内;如果在关闭过程中再次收到信号,则直接立即退出。

上述逻辑与常量SHUTDOWN_TIMEOUT_MS = 8000在 index.js 与 index.js 中完整实现。另外两个部署相关的环境变量也值得留意:SERVER_PORT(默认3000)与SERVER_HOSTNAME(默认0.0.0.0)。


六、错误消息:默认脱敏,按需开启详情

错误响应默认脱敏——内部错误细节不会出现在响应体中,而是记录到控制台日志,同时响应会附带一个随机错误 ID(Bun.randomUUIDv7()的尾部片段)便于在日志中检索。两个开关控制该行为:

  • DISABLE_ERROR_LOGGING=true:关闭错误日志输出;
  • SHOW_ERRORS=true:关闭消息脱敏,把完整的错误序列化详情(名称、消息、堆栈、错误码、原因)暴露在响应体detail字段中。

对应实现见 index.js 的全局onError处理器:VALIDATION与NOT_FOUND两类错误默认不写日志;其余错误默认打印结构化日志(含时间戳、Bun 版本、平台与内存快照),并把troubleshooting指引指向本文档对应章节。


七、HashWX:GPU 抗性的默认 PoW 协议

7.1 协议与默认行为

Standalone 以HashWX作为新建 site key 的默认 challenge 协议。HashWX 是 Cap 的 GPU 抗性 proof-of-work:与固定哈希函数 SHA-256 不同,它每个 challenge 都基于 seed 生成一个全新的单向函数,由整数运算与分支构成,使得 GPU 相对 CPU 几乎没有吞吐优势。

三个关键特性:

  • 按键独立:协议是按 site key 单独设置的,因此可以出现部分键用 SHA-256、部分键用 HashWX 的混合状态;
  • 旧键不受影响:在 HashWX 成为默认值之前创建的键会保留原有协议,直到你手动更改;
  • 无需密钥对:HashWX 不要求生成或保存任何密钥对,配置成本为零。

切换方式:打开该 site key 的Configuration标签页,在Challenge protocol下选择所需协议即可。对应的默认配置在 server.js 的keyDefaults中定义(protocol: "hashwx"、hashwxDifficulty: 1_000_000),API 层允许的协议值为sha256-pow、rsw、hashwx三选一(server.js)。

7.2 难度配置与设备耗时参考

HashWX difficulty滑条控制难度,其数值含义是预期客户端需要执行的哈希次数:

  • 默认值:1_000_000,被拆分为4 个子挑战(sub-challenges);
  • 实测参考:8 核桌面 Chrome 上中位数约578 毫秒;手机上约1.1 到 5.9 秒(调高难度前务必先参考 hashwx.md 中的手机实测数据);
  • 取值范围:50_000到5_000_000。

上述取值范围与默认值同样在 server.js 的 API 校验层(minimum: 50000, maximum: 5000000)与配置默认值中原样体现。

7.3 WebAssembly 前提与回退方案

无法运行 WebAssembly 的客户端无法求解 HashWX challenge。如果需要兼容这类客户端,请把该键切换为SHA-256 PoW——它提供纯 JavaScript 实现作为回退路径。作为对比,cap-core(非 Standalone 场景)默认仍使用 SHA-256 PoW,除非显式开启(参见 capjs-core 的 HashWX 说明)。

7.4 已废弃的 RSW time-lock

RSW(Rivest-Shamir-Wagner time-lock 谜题)仍然可以对存量部署按键启用,但已废弃:GPU 每秒可求解的数量约为 CPU 的170 倍,无法提供设计初衷所期望的 GPU 抗性。相关配置:

  • RSW difficulty滑条设置参数t(需要顺序执行的平方运算次数),范围10_000–300_000,默认75_000(见 server.js 的rswT默认值及其校验范围 server.js);
  • RSW_BITS=2048用于在启动时覆盖 RSA 模数的位宽(对应 rsw-store.js 中的密钥对加载与刷新逻辑)。

小提示:widget 能够根据数据格式自动检测挑战协议,因此当你调整键的协议时,唯一需要做的就是切换键本身,无需修改前端代码。


八、Instrumentation 挑战:拦截自动化与 headless 浏览器

除了 PoW,Cap Standalone 还支持JavaScript instrumentation challenge,用于应对能自动求解 proof-of-work 的攻击者,并可选择性地拦截 headless 浏览器:

  • 默认开启:新建 site key 时,instrumentation challenge 默认启用(对应 server.js 中instrumentation: false为 API 入参默认,而仪表盘新建流程会开启它;配置结构支持每个键独立开关);
  • 开关位置:在 site key 的设置页面中开启或关闭;
  • 拦截 headless:如需屏蔽 headless 浏览器,在该键设置中打开"Attempt to block headless browsers"(对应配置字段blockAutomatedBrowsers,见 server.js);
  • 混淆级别:obfuscationLevel默认3,取值范围 1–10(server.js)。官方建议保持级别 3,除非你需要更强的混淆效果;过高的级别会显著降低成功率。如果觉得级别 3 太慢,级别 1 在单核上会快得多。

九、IP 数据库:国家与 ASN 归属查询

Cap Standalone 的统计与地理功能依赖 IP 归属查询,支持在仪表盘Settings > IP Data > Country & ASN data中从三个提供商中选择:

  1. DB-IP Lite(免费,无需凭据,自动尝试最近 3 个月的数据文件);
  2. MaxMind GeoLite2(需要 MaxMind 账户 ID 与 License Key,通过 Basic Auth 下载 tar.gz 并解包出.mmdb);
  3. IPInfo API(需要 API token,走远程 HTTP 查询,本地不落地文件)。

对于 DB-IP 与 MaxMind,.mmdb文件会被下载到容器内的/usr/src/app/data/目录。对应实现见 ipdb.js(DATA_DIR指向standalone/data,在容器中即/usr/src/app/data),下载、解压、进度上报与每 2 小时重载(RELOAD_INTERVAL)的完整流程均在该文件中。

Docker volume 权限问题

容器以非特权用户bun(UID 1000)运行(见 Dockerfile 的USER bun)。如果你把宿主机目录 bind-mount 到/usr/src/app/data,该目录必须允许 UID 1000 写入,否则下载会失败并报EACCES: permission denied。正确做法:

mkdir -p ./cap-data sudo chown 1000:1000 ./cap-data
services: cap: image: tiago2/cap:latest volumes: - ./cap-data:/usr/src/app/data # ...

如果无法修改宿主机文件属主(部分平台如 Coolify 操作起来较麻烦),最简单的替代方案有三个:

  1. 不做 bind mount,让 Docker 管理数据目录——镜像在构建时已创建好目录并设置了正确属主(Dockerfile 中的mkdir -p data && chown bun:bun data);
  2. 改用named volume代替 bind mount;
  3. 切换到无需本地文件的 IP 数据提供商(即 IPInfo API 模式)。

值得补充的是,ipdb.js 启动时会对数据目录做可写性探测(写入并删除.write-test文件),失败时会直接输出包含chown 1000:1000建议的错误日志,方便你在容器日志中第一时间定位该问题。


总结:生产部署配置速查

结合本文与源码,一份自托管生产部署的最小关注清单如下:

关注点环境变量 / 配置默认值生产建议
跨域CORS_ORIGIN*收窄为你的域名白名单
静态资源ENABLE_ASSETS_SERVER关闭自托管时置true
资源版本WIDGET_VERSION/WASM_VERSIONlatest固定到已发布的具体版本
资源来源CACHE_HOSTjsdelivr CDN可替换为自建镜像
限流仪表盘或PUT /settings/ratelimit30 次 / 5 秒按业务峰值调整,勿直接暴露公网
代理 IPRATELIMIT_IP_HEADER或 Settings > Headers标准三头按代理类型设置并确保转发真实 IP
存储REDIS_URLredis://localhost:6379生产建议独立实例
多实例隔离REDIS_PREFIX空共享实例时务必设置
健康检查/health、/health/live—接入编排系统探针
挑战协议每键 ConfigurationHashWX按客户端兼容性选择
混淆级别obfuscationLevel3保持 3,慢则降 1
IP 数据目录/usr/src/app/data—bind mount 时确保 UID 1000 可写

若需从头部署,请结合 Cap Standalone 快速开始指南(含 Valkey 的 docker-compose 配置)与 API 文档 一起阅读;HashWX 协议的设计原理与成本实测可继续参考 HashWX 详解。

  • 网络安全
  • 应用安全
  • 后端

【免费下载链接】cap

Free, 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
点击查看免费下载

相关推荐

上一篇:Arnis终极部署指南:5种环境配置策略与高效管理技巧
下一篇:最全面Manim版本解析:社区版vs原版核心差异与选择指南

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询