Cloudlrare:turnstile
2026年3月12日
turnstile
Cloudflareの文字とアイコンがある小さめのチェックボックス付きウィジェットで、form操作のような部分的な保護の為に用いる。
スクリーンサイズで表示されるturnstileと似たものはturnstileではない。ドメイン/セキュリティルール/カスタムルールから特定のパスに対してマネージドチャレンジを設定する。
Cloudflareでは最初からDDOSやボット対策はONになっているので、カスタムルールを使うのは特定の国を弾くようなケース
始める
https://developers.cloudflare.com/turnstile
https://developers.cloudflare.com/turnstile/tutorials/login-pages/
Cloudflareのダッシュボード/Turnstileから「ウィジェットを追加」
ホストを追加
localhostとアプリのドメインを追加ドメインはCloudflare以外でもOK(例:
vercel.app)
サイトキー/シークレットキーを
.envへ追加サイトキーには
NEXT_PUBLICをつける
NEXT_PUBLIC_TURNSTILE_SITE_KEY=""
TURNSTILE_SECRET_KEY=""form送信の例
"use client";
import Script from "next/script";
import { createAction } from "./turnstile.action";
const Page = () => {
return (
<div className="max-w-2xl p-10">
<form action={createAction} className="grid gap-4">
<div
className="cf-turnstile"
data-sitekey={process.env.NEXT_PUBLIC_TURNSTILE_SITE_KEY}
/>
<input
type="text"
placeholder="Enter..."
name="content"
className="rounded ring"
/>
<button className="rounded ring">Submit</button>
</form>
<Script
src="https://challenges.cloudflare.com/turnstile/v0/api.js"
async
defer
/>
</div>
);
};
export default Page;scriptが読み込まれると
className="cf-turnstile"がついたdiv内にウィジェットが埋め込まれる。DOMでみるとhidden属性のinputにname="cf-turnstile-response"がある。クライアント側の挙動を突破したという証明のtokenが入っている。*この段階でウィジェットにチェックが入ったとしても、クライアント側での確認が終わったのみ
action.ts:
"use server";
export const createAction = async (formData: FormData) => {
const token = formData.get("cf-turnstile-response");
if (!token) {
throw new Error("Turnstile token missing");
}
const res = await fetch(
"https://challenges.cloudflare.com/turnstile/v0/siteverify",
{
method: "POST",
body: new URLSearchParams({
secret: process.env.TURNSTILE_SECRET_KEY!,
response: token.toString(),
}),
},
);
const data = await res.json();
if (!data.success) {
throw new Error("Bot detected");
}
if (data.hostname !== BASE_URL) {
throw new Error("Invalid hostname");
}
// 処理
};クライアントから渡されたtokenが本当にCloudflareが発行したものか知るために、CloudflareのAPIに問い合わせる。
問い合わせ結果のsuccessやhostnameに応じて処理を分岐する。
// logの中身
{
action: '',
cdata: '',
challenge_ts: '2026-03-12T08:15:17.000Z',
'error-codes': [],
hostname: 'localhost',
metadata: { interactive: true },
success: true
} datareact-turnstile
react-turnstile というライブラリを使う場合
"use client";
import Script from "next/script";
import { createAction } from "./turnstile.action";
import { Turnstile } from "react-turnstile";
import { useState } from "react";
const Page = () => {
const [isHuman, setHuman] = useState(false);
return (
<div className="max-w-2xl p-10">
<form action={createAction}>
<input
type="text"
placeholder="Enter..."
name="content"
className="rounded ring"
/>
<Turnstile
sitekey={process.env.NEXT_PUBLIC_TURNSTILE_SITE_KEY!}
onVerify={() => setHuman(true)}
/>
<button
type="submit"
disabled={!isHuman}
className={!isHuman ? "cursor-not-allowed" : ""}
>
Submit
</button>
</form>
</div>
);
};
export default Page;