Article
Cloudflare Turnstile 为静态博客添加「无感」人机验证
Cloudflare 就是神,不接受反驳
Cloudflare 就是神,不接受反驳
创建 Turnstile 组件
进入 Cloudflare Turnstile 官网,创建组件


NOTE配置建议:
- 模式:Managed
- 域名:你的站点域名
- 预先许可(Cookie):关闭(我们自己控制)

创建完成之后可以看到页面跳转,得到两个值
- Site Key(前端用)
- Secret Key(服务端用)

在 Pages 中配置 Secret Key


NOTE
- Name: TURNSTILE_SECRET(一个字母都不能变)
- Value: xxxxxxxx(Secret Key)

创建 gate 页面(前端)
新建文件:
src/pages/gate.astro这是一个 自动检测 token → 自动跳转 的代码(YOUR_SITE_KEY自行修改):
---const siteKey = "YOUR_SITE_KEY";const next = Astro.url.searchParams.get("next") ?? "/";---
<!doctype html><html lang="zh-CN"> <head> <meta charset="utf-8" /> <meta name="viewport" content="width=device-width,initial-scale=1" /> <title>Human verification</title> <script src="https://challenges.cloudflare.com/turnstile/v0/api.js" async defer></script> </head>
<body class="gate"> <main class="card"> <h1>正在验证</h1> <p class="desc">正常用户将自动通过,请稍候…</p> <div class="cf-turnstile" data-sitekey={siteKey}></div> </main>
<script define:vars={{ next }}> const timer = setInterval(async () => { const tokenEl = document.querySelector( 'input[name="cf-turnstile-response"]' ); const token = tokenEl?.value; if (!token) return;
clearInterval(timer);
const res = await fetch("/api/turnstile-verify", { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify({ token, next }), });
if (res.ok) { const data = await res.json(); location.replace(data.redirect || "/"); } }, 400); </script> </body></html>服务端校验 Turnstile(Pages Functions)
新建文件( functions 文件夹在根目录下自行创建):
functions/api/turnstile-verify.ts// @ts-nocheck
export const onRequestPost = async (ctx) => { const { token, next } = await ctx.request.json().catch(() => ({}));
if (!token) { return new Response("missing token", { status: 400 }); }
const form = new FormData(); form.append("secret", ctx.env.TURNSTILE_SECRET); form.append("response", token);
const ip = ctx.request.headers.get("CF-Connecting-IP"); if (ip) form.append("remoteip", ip);
const verify = await fetch( "https://challenges.cloudflare.com/turnstile/v0/siteverify", { method: "POST", body: form, } );
const result = await verify.json(); if (!result.success) { return new Response("verification failed", { status: 403 }); }
const redirect = typeof next === "string" && next.startsWith("/") ? next : "/";
const cookie = "cf_human=1; Path=/; Max-Age=43200; HttpOnly; Secure; SameSite=Lax";
return new Response(JSON.stringify({ redirect }), { headers: { "content-type": "application/json", "set-cookie": cookie, }, });};全站拦截(Middleware)
新建文件:
functions/_middleware.ts// @ts-nocheck
export const onRequest = async (ctx) => { const url = new URL(ctx.request.url); const path = url.pathname;
if ( path.startsWith("/gate") || path.startsWith("/api/turnstile-verify") || path.startsWith("/_astro/") || path.startsWith("/assets/") || path.startsWith("/favicon") ) { return ctx.next(); }
const cookie = ctx.request.headers.get("Cookie") || ""; const passed = /(?:^|;\s*)cf_human=1(?:;|$)/.test(cookie);
if (!passed) { const next = encodeURIComponent(path + url.search); return Response.redirect(`${url.origin}/gate?next=${next}`, 302); }
return ctx.next();};部署与测试
将修改后的代码 push 到 github
测试流程
- 无痕窗口访问首页 → 自动跳
/gate - 正常浏览器 → 页面一闪直接回首页
- VPN / 异常环境 → 出现 Turnstile 勾选
- 验证成功后:
- Cookie:
cf_human=1 - 12 小时内全站免验证
- Cookie:
Cloudflare Turnstile 为静态博客添加「无感」人机验证
https://anpier.cn/posts/blogfuwaricloudflare-turnstile/