ファランクスブログ

© 2026 all rights reserved.
  1. blog
  2. cloudflare-turnstile
  • turnstile
  • 始める
  • form送信の例
  • react-turnstile

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/

  1. Cloudflareのダッシュボード/Turnstileから「ウィジェットを追加」

  2. ホストを追加

    1. localhost と アプリのドメインを追加

    2. ドメインはCloudflare以外でもOK(例: vercel.app)

  3. サイトキー/シークレットキーを .env へ追加

    1. サイトキーには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
} data

react-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;

cloud
/
cloudflare
security
  • turnstile
  • 始める
  • form送信の例
  • react-turnstile