Loop Library表单网关安全设计:Turnstile、幂等键与限流去重的完整拆解
【免费下载链接】loop-libraryA library of practical AI-agent loops and an installable skill for finding, adapting, and designing repeatable agent workflows.项目地址: https://gitcode.com/gh_mirrors/lo/loop-library
Loop Library 是一个面向 AI Agent 的实用循环(loop)工作流公开库,用户既可以直接投稿 loop,也可以订阅每周精选。这两类表单提交都经过一个精心设计的表单网关:Cloudflare Turnstile 人机校验、幂等键(Idempotency Key)防重复、按 IP 限流与 24 小时内容去重。本文完整拆解这套安全机制的实现思路,帮你理解如何为公开表单搭建可靠的防护体系。
一次提交的完整旅程 🛡️
所有表单请求都由部署在 Cloudflare Worker 上的网关统一处理,入口在 loop-library/worker/src/index.js。一次提交会依次穿过这些关卡:
| 顺序 | 防线 | 拦截目标 |
|---|---|---|
| 1 | CORS 源白名单 | 陌生域名的跨站提交 |
| 2 | 16 KB 请求体上限 | 超大负载攻击 |
| 3 | Honeypot 蜜罐字段 | 自动填表的爬虫 |
| 4 | Turnstile 验证限流 | 刷验证接口的滥用 |
| 5 | Turnstile 服务端复核 | 伪造的人机校验令牌 |
| 6 | 幂等键绑定检查 | 重复/篡改的表单会话 |
| 7 | 每小时 + 每日 IP 限流 | 高频灌水 |
| 8 | 24 小时内容指纹去重 | 同一内容的重复投递 |
全部通过后,网关才把数据写入 here.now 的 Site Data 存储(writeSiteData),并返回201。任何一步失败都会得到带错误码的明确响应,而不是静默失败。
Turnstile 人机校验:几乎无感的防护 ✨
网站为两个表单分别配置了 Turnstile 组件,样式为interaction-only——大多数访问者根本看不到验证弹窗,只有可疑流量才会被挑战。
- 前端:script.js 中渲染验证组件,成功回调后才启用提交按钮;令牌过期会自动重置。
- 服务端:网关并不信任前端,而是拿令牌向 Cloudflare 复核,并做三重匹配(verifyTurnstile):
action必须与该表单一致(投稿用submit_loop,订阅用weekly_signup),防止拿 A 表单的令牌去提交 B 表单;- 校验通过的
hostname必须在TURNSTILE_HOSTNAMES精确白名单内; - 携带客户端 IP 参与校验,提高判断质量。
此外,验证接口本身也有限流:wrangler.jsonc 中为TURNSTILE_RATE_LIMITER配置了30 次 / 60 秒 / IP的配额,超过即返回429与Retry-After: 60,避免验证通道被当放大器滥用。
相关运维规则见 AGENTS.md「Protected forms」。
幂等键:重复提交只算一次 🔁
前端在打开页面时就用crypto.randomUUID()生成一个 UUID v4 作为idempotency_key(makeIdempotencyKey),整个表单会话期间保持不变,提交成功后才换新。
网关拿到令牌后会计算两个 SHA-256 哈希(index.js):
- requestHash:整条记录的指纹,代表"这一份具体数据";
- fingerprint:内容指纹,用于跨会话去重。
幂等键的绑定逻辑在 Durable ObjectFormGuard中(idempotency 方法),规则很精巧:
| 场景 | 判定 | 结果 |
|---|---|---|
| 同键 + 同内容 | 重放(replay) | 按成功处理,返回202,不再写入 |
| 同键 + 不同内容 | 冲突 | 返回409,提示"表单会话无效,请刷新" |
| 新键 | 正常 | 绑定 24 小时后放行 |
这解决了网络抖动、按钮连点、页面重试等场景下的"重复写入"问题:用户侧看到成功,数据侧只落一条。同时把idempotency_key也透传给存储层的Idempotency-Key请求头(L766-L774),让写入端具备第二道防重。
限流:每个 IP 的配额账本 📊
网关为每张表单定义了独立的配额(FORM_DEFINITIONS):
| 表单 | 每小时上限 | 每日上限 | 触发时的提示 |
|---|---|---|---|
Loop 投稿/suggestions | 3 次 | 10 次 | "This connection has reached the submission limit." |
周更订阅/weekly-signups | 5 次 | 10 次 | "This connection has reached the signup limit." |
配额统计由 FormGuard.rate 完成:它记录每个 IP(以哈希存储,不存明文)近 24 小时内的提交事件,滑窗计算小时数与日计数,被限流时返回精确的retryAfter秒数。前端会读取Retry-After响应头,友好地提示"大约 N 分钟后再试"(postProtectedForm)。
注意一个细节:限流检查只在"非重放"请求时发生(L239-L246)——合法重试不占用配额,恶意刷量才会。
内容去重:24 小时内相同内容只留一条 ✂️
限流管"次数",去重管"内容"。每张表单定义了一个指纹函数(fingerprint):
- 投稿表单:
loop_title(小写化)+instructions(小写化并压缩空白); - 订阅表单:邮箱地址(小写化)。
指纹经 SHA-256 后通过 reserveFingerprint 预留,预留窗口为 24 小时。命中已有指纹时:
- 同一个幂等键的重放 → 静默返回
202; - 其他请求 → 同样返回
202(对用户表现为成功),但不会创建第二条记录。
还有个容易被忽略的优雅设计:如果指纹已预留但后续写存储失败,网关会调用 releaseFingerprint 释放预留,用户修改后可以重新提交,不会"占坑"一天。
几层容易被忽略的小防线 🔍
- Honeypot 蜜罐:两个表单都藏了
aria-hidden的 "Company" 隐藏域(index.html),人类看不到也填不到;一旦有值,网关立刻假装成功(202)并丢弃(L176-L178)。 - 最低填写时长:前端强制投稿表单填写满 1.2 秒、订阅表单满 0.8 秒(script.js),快速拦截脚本化提交。
- 严格字段校验:标题 ≤120 字符、正文 ≤3000 字符、X 账号必须归一化为
@handle、来源链接必须 http(s),全部在 validateSuggestion 中完成,超长请求体直接413。 - 源白名单:只接受
signals.forwardfuture.com与*.here.now的 Origin,其余一律403(getCorsHeaders)。
关键文件速查 📁
| 模块 | 路径 |
|---|---|
| 表单网关主入口(限流、幂等、去重调度) | loop-library/worker/src/index.js |
| FormGuard 状态存储(Durable Object) | loop-library/worker/src/index.js#L823-L1109 |
| 前端表单与 Turnstile 集成 | loop-library/site/script.js#L1113-L1240 |
| Worker 部署配置(限流器、Durable Objects) | loop-library/worker/wrangler.jsonc |
| 表单安全运维规则 | AGENTS.md#L65-L89 |
| 网关行为测试 | loop-library/worker/test/index.test.js |
总结:Loop Library 的表单网关用"人机校验 → 幂等键 → 限流 → 内容去重"四层递进式防护,把公开表单最常见的三类问题——机器人灌水、重复提交、恶意刷量——全部挡在存储写入之前,而且每一层失败都有清晰的错误码与用户提示。这套设计对任何需要接收公开表单提交的项目都极具参考价值。
【免费下载链接】loop-libraryA library of practical AI-agent loops and an installable skill for finding, adapting, and designing repeatable agent workflows.项目地址: https://gitcode.com/gh_mirrors/lo/loop-library
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考