EveryInfra

Build in Public · LF-15

验证码 API 怎么选:从类型目录到自有系统的验证闭环

以自有系统和官方测试机制为范围,核对类型、参数与结果形状,分开验收 token、服务端验证、业务操作和费用,避免把返回结果当成验证成功。

接入验证码时,最危险的误判往往不是选错一个名称,而是把“拿到了结果”当成“验证已经通过”。客户端收到非空 token、验证服务认可这个 token、你的业务允许这次操作,是三件不同的事。只验第一件,表单仍可能在后端被拒绝;把第一件直接当作第三件,又可能让业务绕过自己的权限检查。

这篇文章面向自有系统的开发与测试:怎样辨认挑战产品、读取 EveryInfra 当前类型契约、隔离测试配置,并验证失败时系统会停在正确的位置。官方测试环境和明确获准的测试范围也适用。本文不提供操作真实第三方账户、支付、权限或风控挑战的步骤。

先确定你要测试哪一段

如果目标只是确认自家表单在验证成功后能保存、失败后不会写入,优先使用挑战产品官方提供的测试机制。它能给出可控结果,无需把每一次持续集成都变成真实挑战求解。此时引入另一项 API 不会自动增加测试价值,反而会增加一个需要独立验收的依赖。

如果你确实需要评估 EveryInfra 的验证码能力,再把测试拆成两个独立问题:这项 API 是否按约定输入返回正确形状;返回结果是否在获准环境中被正确处理。保留二者各自的证据,不用其中一个的成功替代另一个。选型起点是测试任务,不是目录里列了多少类型。

从集成配置识别产品,不从外观猜

从自有页面的集成代码、后台配置和对应官方文档确认产品、模式、域名与用途。看上去同样是一个勾选框,并不足以决定请求类型;相同产品的不同模式也不应随意互换。为测试留一份明确记录:谁拥有页面、允许测什么、使用哪组测试配置、预期结果是什么。

对于获准但并非自己维护的环境,参数应由环境维护方提供或按获准文档取得。缺少必要上下文时停止,请维护方确认;不要让程序猜一个相近类型或拼造字段。能在页面看到一个公开标识,不代表得到任意自动操作的许可。

例如 Turnstile 的 hostname 配置接收域名,不接收带协议、端口或路径的完整 URL,而且配置父域会覆盖其子域。测试环境应由维护者核对范围,不能为了让一个错误地址通过就放宽配置;这与请求里的 website_url 是两种不同字段。

读取类型、可用性、参数和结果形状

下面是免费、无鉴权的目录读取,不会创建求解任务。示例只挑两种类型,目的是比较契约,不是要求你对两种挑战都进行操作。需要 curl 与 jq;目录读取失败时先处理发现阶段,不继续使用空输出发起业务请求。

只读类型目录
set -o pipefail
curl -fsS --max-time 30 'https://api.everyinfra.com/api/v1/captcha/types' \
  | jq -e '[.types[] |
      select(.type == "turnstile" or .type == "recaptcha_grid") |
      {type, available, required_params, optional_params, solution}
    ] | if length == 2 then . else error("expected types missing") end'

2026 年 9 月 4 日这次公开观察中,turnstile 的必填参数是 website_url 与 website_key,可选参数包括 action、cdata、page_data;solution.shape 是 token,字段为 token。recaptcha_grid 的必填参数为 body 与 question,shape 是 points,字段为 objects。两项当时的 available 均为 true。这是目录声明,不是本次真实挑战的成功证明。

因此,结果处理器应根据确定的 type 与 solution 契约选择分支,而不是无条件取顶层 token,也不是把任何对象强制转成字符串。参数出现在 optional_params 中,只表示契约声明它是可选项;某种组合能否满足仍要按当前错误和可用性判断,不能靠删除业务必需的约束来换取成功。

把本次目录观察时间与选定条目一起保存。available 为 false 时不要提交;字段缺失、类型不对或读取失败时记为未知,同样不继续。即使它为 true,也只说明观察时可尝试该能力,不保证后续每个任务成功。

把官方测试机制与生产配置隔离

Cloudflare Turnstile 提供测试 sitekey 与测试 secret,可模拟通过、失败或已使用的 token。测试与生产的 token、验证密钥不能混用。Google 的 reCAPTCHA FAQ 则分别说明了 v2 测试键与 v3 独立测试配置;v3 的测试环境评分不能当作真实流量下的风险表现。

sitekey 是页面使用的公开标识,secret 是后端验证密钥。测试配置要从明确的环境配置读取,不能仅凭请求传来“这是测试”就切换到始终通过的行为。生产启动时检查配置来源和环境一致性;日志只保留核查所需的状态,不输出密钥、完整 token 或原始请求对象。

建议把验证服务放在业务代码的一个窄接口后:业务只接收明确的验证结果,测试可以注入合成结果;真正对外验证则由服务端实现。注入机制只能由测试构建或服务器配置控制,不能暴露为用户可选择的请求参数。这是应用设计建议,不是 EveryInfra 自动提供的功能。

token、服务端验证和业务操作分开验收

Turnstile 官方要求服务端调用 Siteverify;仅有前端组件不构成完整保护。官方文档还规定 token 有效期为五分钟且只能验证一次。不要把 token 当成可长期缓存、反复重放的凭证,网络失败也不能靠永远重用同一个值来恢复。

即使服务端验证成功,业务仍要执行自己的登录态、资源权限、输入校验与重复提交控制。验证码不是账户凭据,也不是交易授权。业务后续失败时保留业务错误,不能把它重命名为“验证码失败”再无上限创建新任务。

不同产品的验证细节也不能互抄。Google reCAPTCHA 的后端验证文档给出两分钟、单次使用的 token 规则,以及 timeout-or-duplicate 等错误;它与上文 Turnstile 的五分钟窗口不同。错误处理要绑定具体产品和实际验证响应。

先用离线例子检查自有后端的接受条件

下面只演示一个自有测试页面的结果检查器。该页面明确配置 hostname 为 localhost、action 为 test;期望值来自服务端固定配置,不取自用户请求。传入的 result 在真实集成中必须由你自己的服务端调用官方验证接口获得,绝不能直接信任客户端提交的 success。

JavaScript:合成验证结果,不发起网络请求
function acceptOwnTestVerification(result, expected) {
  if (!expected || typeof expected.hostname !== "string" ||
      !expected.hostname.trim() || typeof expected.action !== "string" ||
      !expected.action.trim()) {
    throw new Error("server-side test configuration required");
  }
  if (!result || typeof result !== "object" || Array.isArray(result)) {
    return { accepted: false, reason: "invalid_response" };
  }
  if (result.success !== true) {
    return { accepted: false, reason: "verification_failed" };
  }
  if (result.hostname !== expected.hostname) {
    return { accepted: false, reason: "hostname_mismatch" };
  }
  if (result.action !== expected.action) {
    return { accepted: false, reason: "action_mismatch" };
  }
  return { accepted: true, reason: "verification_only" };
}

// 全部为自造测试值;不请求网络,不持有或生成 token。
const expected = { hostname: "localhost", action: "test" };
const synthetic = { success: true, hostname: "localhost", action: "test" };
console.log(acceptOwnTestVerification(synthetic, expected));
// { accepted: true, reason: 'verification_only' }

这个例子只输出 verification_only:它不发出 token、不调用 Siteverify,也不提交表单。检查器强制 action 的原因是这份测试配置明确需要它;不要误解为所有挑战产品都返回同样字段。真实产品不同,就分别实现该产品的验证契约,而不是复用一个放宽判断的通用函数。

至少把成功、success 为 false、success 是字符串、域名错误、action 缺失、响应为空和服务端配置缺失各测一次。然后在获准的官方测试集成中,再核对从页面到自有后端的完整路径。离线函数通过只能证明这组分支判断,不能证明网络验证、生产配置或风险识别效果。

按失败发生的位置判断下一步

  • 输入阶段:类型不匹配、缺必填值或参数组合不支持。先修正契约,不把同一请求原样循环发送。
  • 能力阶段:目录未知或当前不可用。停止该项测试并记录观察,不擅自替换挑战产品。
  • 调用阶段:明确失败与超时未决分别记录。未收到结果不等于服务端没有执行,先保留请求关联信息。
  • 验证阶段:自有后端拒绝结果。记录安全的错误分类与对应测试用例,检查环境、域名和 action,而不是放宽接受条件。
  • 业务阶段:验证通过后仍可能因输入、权限或重复提交被拒绝。由业务流程恢复,不冒充验证码未解出。

费用也要按这一分层核对。当前 EveryInfra 实现对求解失败分支安排退款,但客户端自己的验证失败或业务拒绝,不等于网关已经判定求解失败。不能从前端红字推导自动退款;需要按调用响应与可取得的账务记录核对同一次请求。本篇没有新增真实失败退款样本,也不承诺所有后续拒绝都自动退费。

最后留下可以复核的测试记录

一份有用的验收记录应说明:授权环境、产品模式、目录观察时间、所用测试配置类别、输入结构、响应结构、验证判定、业务判定与账务判定。真实内容尽量不留;用脱敏关联标识把同一次测试的各层结果串起来。明确写出没有覆盖的环节,避免未来的人把“目录读通了”误认作完整验收。

从官方测试机制验证你自己的成功与失败流程,再评估确有需求的 API 分支,最后才在授权范围内扩大验证。选择验证码类型的标准不是名称相近或某次响应有值,而是输入条件明确、返回形状可解释、后端判定正确,并且失败时不会越过业务边界。