Cloudflare研究所 > Turnstileをフォームに導入する|画面表示とサーバー検証の実装ポイント

Turnstileをフォームに導入する|画面表示とサーバー検証の実装ポイント

2026年10月22日

•約8分で読めます

Cloudflare Turnstileを問い合わせフォームへ組み込む手順を解説。Siteverify、hostname・actionの照合、期限切れと再送、テストキー、保存処理とのつなぎ方を整理します。

Turnstile
フォーム
セキュリティ
Cloudflare Workers
助手
助手

フォームにTurnstileの表示を追加しました。これで迷惑な送信を防げますか。

博士
博士

表示は入口じゃ。送られたトークンをサーバーで検証し、成功した場合だけ保存処理へ進める必要がある。

Turnstileの導入は、ブラウザーのウィジェットと、サーバーからのSiteverify呼び出しを組み合わせます。 ウィジェットが表示されても、受信側が検証しなければ、そのURLへ直接送られたリクエストを止められません。公式:サーバー側の検証

この記事では、自社の問い合わせフォームを例に、トークンの受け渡しから検証後の保存までを整理します。コード例は検証の流れを理解するためのもので、実際の問い合わせ保存やメール送信は行いません。

フォームの表示からトークンの検証を経て、成功時だけ保存へ進む図

画面に置くキーと、サーバーで守るキーを分ける

ダッシュボードでTurnstileのウィジェットを作り、対象ホスト名と表示方式を設定します。検証用と本番用で設定を分けると、公開前の確認で本番の許可範囲を広げずに済みます。

項目 置く場所・役割
Sitekey ブラウザーに渡す公開用の識別子
Secret key サーバーのSecretsに保存する検証用の秘密情報
許可ホスト名 ウィジェットを使用するサイトの範囲
action 問い合わせ、登録など、処理の用途を区別する値

Secret keyをHTML、公開環境変数、Gitのソース、ブラウザーの通信へ含めないでください。WorkersならSecretとして登録し、サーバー側から参照します。Sitekeyは秘密情報ではありませんが、意図したホスト名で使われる設定にします。公式:ウィジェットの作成

フォームにはウィジェットと送信先を設定する

次は、入力されたメールアドレスとTurnstileのトークンを検証用エンドポイントへ送る例です。YOUR_SITE_KEY は自分のSitekeyに置き換えます。

<script src="https://challenges.cloudflare.com/turnstile/v0/api.js"
        async defer></script>

<form action="/contact-check" method="post">
  <label>
    メールアドレス
    <input name="email" type="email" required>
  </label>
  <div class="cf-turnstile"
       data-sitekey="YOUR_SITE_KEY"
       data-action="contact"></div>
  <button type="submit">認証を確認する</button>
</form>

通常のフォーム内に配置すると、既定では cf-turnstile-response という名前の入力が生成され、トークンを送信できます。JavaScriptで独自送信する場合は、その値を送信データに含めます。公式:クライアント側の実装

ウィジェットを手動で描画する構成や、画面遷移の多いアプリでは、初期化と破棄、再表示のタイミングも確認します。公式スクリプトを独自にコピーして固定せず、案内されたURLから読み込みます。

サーバーでは成功・ホスト名・用途を照合する

受信したトークンは、サーバーからCloudflareのSiteverify APIへ送ります。有効期限は5分で、同じトークンは1回しか検証に使えません。 長く入力していたフォームや、二重送信では取り直しが必要になります。公式:検証仕様

次のWorkers例は、検証だけを行う学習用です。TURNSTILE_SECRET_KEY をSecretに、TURNSTILE_HOSTNAME を実際のホスト名に設定します。ホスト名には https:// やパスを含めません。

export default {
  async fetch(request, env) {
    const reply = (data, status) => Response.json(data, {
      status,
      headers: { "Cache-Control": "no-store" }
    });
    const url = new URL(request.url);
    if (url.pathname !== "/contact-check") {
      return reply({ error: "Not found" }, 404);
    }
    if (request.method !== "POST") {
      return reply({ error: "POST required" }, 405);
    }
    if (!env.TURNSTILE_SECRET_KEY || !env.TURNSTILE_HOSTNAME) {
      return reply({ error: "Verification unavailable" }, 503);
    }

    let form;
    try {
      form = await request.formData();
    } catch {
      return reply({ error: "Invalid form" }, 400);
    }
    const token = form.get("cf-turnstile-response");
    if (typeof token !== "string" || !token || token.length > 2048) {
      return reply({ error: "New verification required" }, 400);
    }

    try {
      const response = await fetch(
        "https://challenges.cloudflare.com/turnstile/v0/siteverify",
        {
          method: "POST",
          headers: { "Content-Type": "application/json" },
          body: JSON.stringify({
            secret: env.TURNSTILE_SECRET_KEY,
            response: token
          }),
          signal: AbortSignal.timeout(10000)
        }
      );
      if (!response.ok) {
        return reply({ error: "Verification unavailable" }, 503);
      }
      const result = await response.json();
      if (result.success !== true ||
          result.hostname !== env.TURNSTILE_HOSTNAME ||
          result.action !== "contact") {
        return reply({ error: "New verification required" }, 403);
      }
      return reply({
        verified: true,
        stored: false,
        message: "認証確認のみ。問い合わせは保存していません。"
      }, 200);
    } catch {
      return reply({ error: "Verification unavailable" }, 503);
    }
  }
};

success だけでなく、自分のホスト名と用途で発行された結果かを確認しています。通信障害や設定不足のときは、検証を飛ばして受理する処理にしません。コードは仕様に沿った実装例であり、読者の環境で動作確認済みという意味ではありません。

検証の後に、入力確認と保存処理をつなぐ

Turnstileは、問い合わせの内容、利用者の権限、重複保存まで判定する機能ではありません。本番のフォームでは次の順序を設計します。

  1. 受信サイズと形式を制限し、必要な項目があるか確認する。
  2. Turnstileのトークンをサーバーで検証する。
  3. 文字数・メール形式・許可する項目をサーバー側で確認する。
  4. 必要な認証・認可、CSRF対策、送信頻度の制限を行う。
  5. 重複処理を防ぎながら保存し、必要な通知を実行する。
  6. 保存結果に応じた画面を返す。

実際の順序はフォームの認証方式などで調整します。Turnstileの検証成功と、問い合わせの保存成功は別の結果として扱います。保存に失敗したのに「送信が完了しました」と表示しないようにしてください。

問い合わせIDなどによる重複防止も別途必要です。トークンが1回限りだからといって、利用者が新しいトークンで同じ内容を送ることまでは防げません。ログにはエラー分類や問い合わせIDを残し、Secretや生のトークンを記録しない運用にします。

期限切れ・拒否・通信障害を含めて確認する

公式のテスト用Sitekey・Secret keyを使うと、成功や失敗の挙動を再現できます。両方を対応するテスト用の組み合わせにし、本番設定とは分離します。公式:テスト方法

確認するケース 期待する動き
正常なトークン 検証後の入力確認・保存へ進む
トークン未送信 保存せず、認証をやり直す案内を出す
有効期限切れ 入力内容を保ち、新しいトークンで再送できる
同じトークンの再利用 検証失敗として処理する
別のhostname・action 想定したフォームの結果ではないため拒否する
Siteverifyの通信障害 保存せず、時間を置いて再試行する案内を出す
保存処理だけ失敗 認証成功と送信完了を混同しない

固定結果を返すテストキーだけでは、本番のhostname・actionとの照合や実環境の通信条件をすべて確認できません。許可した検証ドメインの実ウィジェットでも確認します。

スマートフォン、キーボード操作、長時間の入力、戻る操作、JavaScriptの読み込み失敗も対象です。期限切れ時に入力内容を消さず、再検証する方法を分かるように表示すると、利用者が同じ文章を何度も書かずに済みます。

よくある質問

Cloudflareをサイト全体で使っていなくても導入できますか?

Turnstileは、Cloudflare経由で配信していないサイトにも組み込めます。フォームの受信側でSiteverifyを呼べること、ウィジェットの設定とスクリプトの読み込み条件を満たすことを確認します。公式:Turnstile

ボタンを押せないようにするだけでも効果がありますか?

利用者の操作案内には役立ちますが、受信URLへの直接送信は止められません。サーバー側で毎回検証し、成功したリクエストだけを次の処理へ進めます。

スパムを完全になくせますか?

完全な排除を保証する仕組みではありません。入力検証、送信頻度の制限、重複防止、ログの確認と組み合わせ、正当な問い合わせを拒否していないかも確認します。

次に読む記事

※仕様と公式資料の確認日:2026年10月22日。設定画面やAPIの仕様は変更されるため、実装時にはリンク先の最新情報を確認してください。

Web・AI・データ連携

Cloudflare無料ホワイトペーパー

導入判断の確認項目を、無料資料で

Web更新やデータ連携の構成例をもとに、費用・権限・運用の確認項目を整理できる無料資料です。

Cloudflareで、WebとAIを業務につなぐ。構成例と導入の進め方、費用・権限のチェックリスト。無料PDF・全16ページ。無料で資料を受け取る。
著者・運営会社
著者
守高 成悟
守高 成悟

代表取締役 CEO

千葉県出身。10歳の頃からプログラミングを始め、ゲーム、Webサイト、ロボット、スマホアプリなどを制作。大阪大学基礎工学部情報科学科で情報工学と統計学を学び、大学時代はAIを研究。大学在学中にWeb広告代理店でのインターンや人材系Webサービスの立ち上げを経験し、卒業後はフリーランスエンジニアとしてGISシステム、データ基盤構築、Webシステムの開発に従事。10年以上のWebアプリ開発・データ分析経験を基に、2023年9月に株式会社ビットライトを設立し、現場業務の仕組み化からデータ基盤構築、データ活用支援までを一気通貫で支援。

運営会社
株式会社ビットライト
株式会社ビットライト

顧客が本当に必要だった価値を、実装する。

現場業務の仕組み化からデータ基盤構築、データ活用支援までを一気通貫で支援しています。

コーポレートサイトを見る