mcpField
C
cloudflare-turnstile

cloudflare-turnstile

github.com/jackspaceセキュリティ1642 回インストール12 週間前に更新
リポジトリ

cloudflare-turnstile について

Cloudflare Turnstile をウェブフォームやアプリに組み込むための開発スキルです。ウィジェット設定、クライアント側の描画、サーバー側のトークン検証、フレームワーク別の例、テスト、トラブルシューティングを扱います。

主な機能

  1. 01Turnstile ウィジェットの設定
  2. 02フォームや SPA での描画
  3. 03サーバー側でのトークン検証
  4. 04React と Cloudflare Workers の実装例
  5. 05テスト用キーの利用
  6. 06エラーと CSP の問題解決

活用シーン

  1. 01ウェブフォームにボット検証を追加する
  2. 02React や Next.js アプリに Turnstile を組み込む
  3. 03バックエンドで送信トークンを検証する
  4. 04自動テストでウィジェットの動作を確認する

インストール

コマンドをコピーしてターミナルで実行します。

$npx skills add https://github.com/jackspace/claudeskillz --skill cloudflare-turnstile

インストール後、Claude Code で/cloudflare-turnstileを入力して読み込まれたか確認します。

同梱ファイルREADME.mdSKILL.json

詳細

Cloudflare Turnstile

ステータス: 本番環境で使用可能 最終更新日: 2025-10-22 依存関係: なし(任意:React 用の @marsidev/react-turnstile) 最新バージョン: @marsidev/[email protected], [email protected]


クイックスタート(10 分)

1. Turnstile ウィジェットを作成する

Cloudflare ダッシュボードからサイトキーとシークレットキーを取得します。

# Navigate to: https://dash.cloudflare.com/?to=/:account/turnstile
# Create new widget → Copy sitekey (public) and secret key (private)

重要な理由:

  • 各ウィジェットには固有のサイトキーとシークレットキーのペアがあります
  • サイトキーはフロントエンドに配置します(公開)
  • シークレットキーはバックエンドのみに配置します(非公開)
  • 開発環境/ステージング環境/本番環境では、異なるウィジェットを使用します
2. フロントエンドにウィジェットを追加する

Turnstile ウィジェットを HTML フォームに埋め込みます。

<!DOCTYPE html>
<html>
<head>
  <script src="https://challenges.cloudflare.com/turnstile/v0/api.js" async defer></script>
</head>
<body>
  <form id="myForm" action="/submit" method="POST">
    <input type="email" name="email" required>
    
    <div class="cf-turnstile" data-sitekey="YOUR_SITE_KEY"></div>
    <button type="submit">Submit</button>
  </form>
</body>
</html>

重要:

  • api.js をプロキシしたりキャッシュしたりしないでください。Cloudflare CDN から読み込む必要があります
  • ウィジェットはトークンを含む非表示の入力欄 cf-turnstile-response を自動作成します
  • トークンの有効期限は 5 分です
  • 各トークンは 1 回のみ使用できます
3. サーバーでトークンを検証する

必ずサーバー側でトークンを検証してください。クライアント側の検証だけでは安全ではありません。

// Cloudflare Workers example
export default {
  async fetch(request: Request, env: Env): Promise<Response> {
    const formData = await request.formData()
    const token = formData.get('cf-turnstile-response')
    const ip = request.headers.get('CF-Connecting-IP')

    // Validate token with Siteverify API
    const verifyFormData = new FormData()
    verifyFormData.append('secret', env.TURNSTILE_SECRET_KEY)
    verifyFormData.append('response', token)
    verifyFormData.append('remoteip', ip)

    const result = await fetch(
      'https://challenges.cloudflare.com/turnstile/v0/siteverify',
      {
        method: 'POST',
        body: verifyFormData,
      }
    )

    const outcome = await result.json()

    if (!outcome.success) {
      return new Response('Invalid Turnstile token', { status: 401 })
    }

    // Token valid - proceed with form processing
    return new Response('Success!')
  }
}

3 ステップのセットアップ手順

ステップ 1:ウィジェット設定を作成する
  1. Cloudflare ダッシュボードにログインします
  2. Turnstile セクションに移動します
  3. 「Add Site」をクリックします
  4. 次の項目を設定します:
    • ウィジェットモード:Managed(推奨)、Non-Interactive、または Invisible
    • ドメイン:許可するホスト名を追加します(例:example.com、開発用の localhost)
    • 名前:わかりやすい名前を設定します(例:「本番環境ログインフォーム」)

重要なポイント:

  • 開発環境/ステージング環境/本番環境では、別々のウィジェットを使用します
  • ドメインは、自分が管理しているものだけに制限します
  • Managed モードは、セキュリティとユーザー体験のバランスに優れています
  • ローカルテストでは、localhost を明示的に追加する必要があります
ステップ 2:クライアント側の統合

暗黙的レンダリングまたは明示的レンダリングを選択します:

暗黙的レンダリング(静的フォームにおすすめ):


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


<div class="cf-turnstile"
     data-sitekey="YOUR_SITE_KEY"
     data-callback="onSuccess"
     data-error-callback="onError"></div>

<script>
function onSuccess(token) {
  console.log('Turnstile success:', token)
}

function onError(error) {
  console.error('Turnstile error:', error)
}
</script>

明示的レンダリング(SPA/動的 UI 向け):

// 1. Load script with explicit mode
<script src="https://challenges.cloudflare.com/turnstile/v0/api.js?render=explicit" defer></script>

// 2. Render programmatically
const widgetId = turnstile.render('#container', {
  sitekey: 'YOUR_SITE_KEY',
  callback: (token) => {
    console.log('Token:', token)
  },
  'error-callback': (error) => {
    console.error('Error:', error)
  },
  theme: 'auto',
  execution: 'render', // or 'execute' for manual trigger
})

// Control lifecycle
turnstile.reset(widgetId)        // Reset widget
turnstile.remove(widgetId)       // Remove widget
turnstile.execute(widgetId)      // Manually trigger challenge
const token = turnstile.getResponse(widgetId) // Get current token

React との統合(@marsidev/react-turnstile を使用):

import { Turnstile } from '@marsidev/react-turnstile'

export function MyForm() {
  const [token, setToken] = useState<string>()

  return (
    <form>
      <Turnstile
        siteKey={TURNSTILE_SITE_KEY}
        onSuccess={setToken}
        onError={(error) => console.error(error)}
      />
      <button disabled={!token}>Submit</button>
    </form>
  )
}
ステップ 3:サーバー側の検証

必須:トークンを検証するため、必ず Siteverify API を呼び出します。

interface TurnstileResponse {
  success: boolean
  challenge_ts?: string
  hostname?: string
  error-codes?: string[]
  action?: string
  cdata?: string
}

async function validateTurnstile(
  token: string,
  secretKey: string,
  options?: {
    remoteip?: string
    idempotency_key?: string
    expectedAction?: string
    expectedHostname?: string
  }
): Promise<TurnstileResponse> {
  const formData = new FormData()
  formData.append('secret', secretKey)
  formData.append('response', token)

  if (options?.remoteip) {
    formData.append('remoteip', options.remoteip)
  }

  if (options?.idempotency_key) {
    formData.append('idempotency_key', options.idempotency_key)
  }

  const response = await fetch(
    'https://challenges.cloudflare.com/turnstile/v0/siteverify',
    {
      method: 'POST',
      body: formData,
    }
  )

  const result = await response.json<TurnstileResponse>()

  // Additional validation
  if (result.success) {
    if (options?.expectedAction && result.action !== options.expectedAction) {
      return { success: false, 'error-codes': ['action-mismatch'] }
    }

    if (options?.expectedHostname && result.hostname !== options.expectedHostname) {
      return { success: false, 'error-codes': ['hostname-mismatch'] }
    }
  }

  return result
}

// Usage in Cloudflare Worker
const result = await validateTurnstile(
  token,
  env.TURNSTILE_SECRET_KEY,
  {
    remoteip: request.headers.get('CF-Connecting-IP'),
    expectedHostname: 'example.com',
  }
)

if (!result.success) {
  return new Response('Turnstile validation failed', { status: 401 })
}

重要なルール

必ず行うこと

✅ Siteverify API を呼び出す - サーバー側での検証は必須です ✅ HTTPS を使用する - HTTP 経由で検証しないでください ✅ シークレットキーを保護する - フロントエンドのコードに公開しないでください ✅ トークンの有効期限切れに対処する - トークンの有効期限は 5 分です ✅ エラーコールバックを実装する - 失敗に適切に対処してください ✅ テストにはダミーキーを使用する - テスト用サイトキー:1x00000000000000000000AA ✅ 妥当なタイムアウトを設定する - 検証を無期限に待たないでください ✅ アクション/ホスト名を検証する - 指定されている場合は追加フィールドも確認してください ✅ キーを定期的にローテーションする - ダッシュボードまたは API を使ってシークレットをローテーションします ✅ 分析データを監視する - 解決率と失敗を追跡します

絶対に行わないこと

❌ サーバー側の検証を省略する - クライアント側だけの検証はセキュリティ上の脆弱性になります ❌ api.js スクリプトをプロキシする - Cloudflare CDN から読み込む必要があります ❌ トークンを再利用する - 各トークンは 1 回のみ使用できます ❌ GET リクエストを使用する - Siteverify が受け付けるのは POST のみです ❌ シークレットキーを公開する - シークレットはバックエンドの環境内だけに保管してください ❌ クライアント側の検証を信頼する - トークンは偽造される可能性があります ❌ api.js をキャッシュする - 今後の更新によって統合が動作しなくなる可能性があります ❌ テストで本番環境のキーを使用する - 代わりにダミーキーを使用してください ❌ エラーコールバックを無視する - 必ず失敗に対処してください


既知の問題の予防

このスキルは、記録されている 12 件の問題を防止します:

問題 #1:サーバー側の検証がない

エラー:Turnstile Analytics ダッシュボードでトークンの検証数がゼロになる ソース:https://developers.cloudflare.com/turnstile/get-started/ 発生する理由:開発者がクライアント側のウィジェットだけを実装し、Siteverify の呼び出しを省略する 予防策:すべてのテンプレートに、Siteverify API を使った必須のサーバー側検証が含まれています

Issue #2:トークンの有効期限切れ(5 分)

エラー:遅延後に送信された有効なトークンに対して success: false が返されます 出典:https://developers.cloudflare.com/turnstile/get-started/server-side-validation 原因:トークンは生成から 300 秒後に有効期限が切れます 予防策:テンプレートに有効期限を記載し、有効期限切れ時のトークン更新を実装します

Issue #3:フロントエンドでのシークレットキーの露出

エラー:セキュリティが回避され、攻撃者が自身のトークンを検証できてしまいます 出典:https://developers.cloudflare.com/turnstile/get-started/server-side-validation 原因:シークレットキーが JavaScript にハードコードされている、またはソース内で確認できる状態になっています 予防策:すべてのテンプレートで、環境変数を使用したバックエンドのみでの検証を示します

Issue #4:Siteverify への GET リクエスト

エラー:API が 405 Method Not Allowed を返します 出典:https://developers.cloudflare.com/turnstile/migration/recaptcha 原因:reCAPTCHA は GET をサポートしていますが、Turnstile には POST が必要です 予防策:テンプレートでは FormData または JSON 本文を使用した POST を使用します

Issue #5:コンテンツセキュリティポリシーによるブロック

エラー:エラー 200500 -「読み込みエラー:iframe を読み込めませんでした」 出典:https://developers.cloudflare.com/turnstile/troubleshooting/client-side-errors/error-codes 原因:CSP が challenges.cloudflare.com の iframe をブロックしています 予防策:スキルに CSP 設定のリファレンスと check-csp.sh スクリプトが含まれています

Issue #6:ウィジェットのクラッシュ(エラー 300030)

エラー:正当なユーザーに対して一般的なクライアント実行エラーが発生します 出典:https://community.cloudflare.com/t/turnstile-is-frequently-generating-300x-errors/700903 原因:不明です。Cloudflare 側の問題と思われます(2025 年) 予防策:テンプレートでエラーコールバック、再試行ロジック、フォールバック処理を実装します

Issue #7:設定エラー(エラー 600010)

エラー:ウィジェットで「設定エラー」が発生します 出典:https://community.cloudflare.com/t/repeated-cloudflare-turnstile-error-600010/644578 原因:ウィジェット設定でホスト名が未設定、または削除されています 予防策:テンプレートにホスト名許可リストの要件と確認手順を記載します

Issue #8:Safari 18/macOS 15 の「IP アドレスを非公開」問題

エラー:Safari の「IP アドレスを非公開」が有効な場合にエラー 300010 が発生します 出典:https://community.cloudflare.com/t/turnstile-is-frequently-generating-300x-errors/700903 原因:プライバシー設定がチャレンジのシグナルに干渉しています 予防策:エラー処理のリファレンスに Safari の回避策(「IP アドレスを非公開」を無効にする)が記載されています

Issue #9:Brave ブラウザーでの紙吹雪アニメーションの失敗

エラー:成功アニメーション中に検証が失敗します 出典:https://github.com/brave/brave-browser/issues/45608(2025 年 4 月) 原因:Brave のシールドがアニメーションスクリプトをブロックします 予防策:テンプレートでアニメーションの完了前に成功を処理します

Issue #10:Next.js と Jest の非互換性

エラー:@marsidev/react-turnstile により Jest のテストが失敗します 出典:https://github.com/marsidev/react-turnstile/issues/112(2025 年 10 月) 原因:Jest でのモジュール解決の問題です 予防策:テストガイドに Jest のモックパターンとダミーの sitekey の使用方法が含まれています

Issue #11:許可リストに localhost が含まれていない

エラー:エラー 110200 -「不明なドメイン:ドメインが許可されていません」 出典:https://developers.cloudflare.com/turnstile/troubleshooting/client-side-errors/error-codes 原因:許可リストに localhost がない状態で、開発環境に本番用ウィジェットを使用しています 予防策:テンプレートでは開発環境にダミーのテストキーを使用し、localhost を許可リストに追加する要件を記載します

Issue #12:トークンの再利用

エラー:「トークンはすでに使用済みです」というエラーとともに success: false が返されます 出典:https://developers.cloudflare.com/turnstile/troubleshooting/testing 原因:各トークンは一度しか検証できません 予防策:テンプレートに一度限りの使用という制約とトークン更新のパターンを記載します


設定ファイルのリファレンス

wrangler.jsonc(Cloudflare Workers)
{
  "name": "my-app",
  "main": "src/index.ts",
  "compatibility_date": "2025-10-22",

  // Public sitekey (safe to commit)
  "vars": {
    "TURNSTILE_SITE_KEY": "1x00000000000000000000AA" // Use real key in production
  },

  // Secret key (DO NOT commit - use wrangler secret)
  // Run: wrangler secret put TURNSTILE_SECRET_KEY
  "secrets": ["TURNSTILE_SECRET_KEY"]
}

これらの設定の理由:

  • 公開サイトキー(クライアントコードから見える)には vars を使用します
  • 非公開のシークレットキー(暗号化され、バックエンドのみで使用)には secrets を使用します
  • 開発にはダミーキーを使用します(testing-guide.md を参照)
  • 本番環境のシークレットキーは四半期ごとにローテーションします
必須の CSP ディレクティブ
<meta http-equiv="Content-Security-Policy" content="
  script-src 'self' https://challenges.cloudflare.com;
  frame-src 'self' https://challenges.cloudflare.com;
  connect-src 'self' https://challenges.cloudflare.com;
">

よく使用するパターン

パターン 1:Hono + Cloudflare Workers
import { Hono } from 'hono'

type Bindings = {
  TURNSTILE_SECRET_KEY: string
  TURNSTILE_SITE_KEY: string
}

const app = new Hono<{ Bindings: Bindings }>()

app.post('/api/login', async (c) => {
  const body = await c.req.formData()
  const token = body.get('cf-turnstile-response')

  if (!token) {
    return c.text('Missing Turnstile token', 400)
  }

  // Validate token
  const verifyFormData = new FormData()
  verifyFormData.append('secret', c.env.TURNSTILE_SECRET_KEY)
  verifyFormData.append('response', token.toString())
  verifyFormData.append('remoteip', c.req.header('CF-Connecting-IP') || '')

  const verifyResult = await fetch(
    'https://challenges.cloudflare.com/turnstile/v0/siteverify',
    {
      method: 'POST',
      body: verifyFormData,
    }
  )

  const outcome = await verifyResult.json<{ success: boolean }>()

  if (!outcome.success) {
    return c.text('Invalid Turnstile token', 401)
  }

  // Process login
  return c.json({ message: 'Login successful' })
})

export default app

使用する場面:Hono フレームワークを使用する Cloudflare Workers の API ルート

パターン 2:React + Next.js App Router
'use client'

import { Turnstile } from '@marsidev/react-turnstile'
import { useState } from 'react'

export function ContactForm() {
  const [token, setToken] = useState<string>()
  const [error, setError] = useState<string>()

  async function handleSubmit(e: React.FormEvent<HTMLFormElement>) {
    e.preventDefault()

    if (!token) {
      setError('Please complete the challenge')
      return
    }

    const formData = new FormData(e.currentTarget)
    formData.append('cf-turnstile-response', token)

    const response = await fetch('/api/contact', {
      method: 'POST',
      body: formData,
    })

    if (!response.ok) {
      setError('Submission failed')
      return
    }

    // Success
  }

  return (
    <form onSubmit={handleSubmit}>
      <input name="email" type="email" required />
      <textarea name="message" required />

      <Turnstile
        siteKey={process.env.NEXT_PUBLIC_TURNSTILE_SITE_KEY!}
        onSuccess={setToken}
        onError={() => setError('Challenge failed')}
        onExpire={() => setToken(undefined)}
      />

      {error && <div className="error">{error}</div>}

      <button type="submit" disabled={!token}>
        Submit
      </button>
    </form>
  )
}

使用する場面:React フックを使用する Next.js のクライアント側フォーム

パターン 3:ダミーキーを使用した E2E テスト
// test/helpers/turnstile.ts
export const TEST_TURNSTILE = {
  sitekey: {
    alwaysPass: '1x00000000000000000000AA',
    alwaysBlock: '2x00000000000000000000AB',
    invisible: '1x00000000000000000000BB',
    interactive: '3x00000000000000000000FF',
  },
  secretKey: {
    alwaysPass: '1x0000000000000000000000000000000AA',
    alwaysFail: '2x0000000000000000000000000000000AA',
    tokenSpent: '3x0000000000000000000000000000000AA',
  },
  dummyToken: 'XXXX.DUMMY.TOKEN.XXXX',
}

// Playwright test example
test('form submission with Turnstile', async ({ page }) => {
  // Set test environment
  await page.goto('/contact?test=true')

  // Widget uses test sitekey in test mode
  await page.fill('input[name="email"]', '[email protected]')

  // Turnstile auto-solves with dummy token
  await page.click('button[type="submit"]')

  await expect(page.locator('.success')).toBeVisible()
})

使用する場面:自動テスト(Playwright、Cypress、Jest)

パターン 4:ウィジェットのライフサイクル管理
class TurnstileManager {
  private widgetId: string | null = null
  private sitekey: string

  constructor(sitekey: string) {
    this.sitekey = sitekey
  }

  render(containerId: string, callbacks: {
    onSuccess: (token: string) => void
    onError: (error: string) => void
  }) {
    if (this.widgetId !== null) {
      this.reset() // Reset if already rendered
    }

    this.widgetId = turnstile.render(containerId, {
      sitekey: this.sitekey,
      callback: callbacks.onSuccess,
      'error-callback': callbacks.onError,
      'expired-callback': () => this.reset(),
    })

    return this.widgetId
  }

  reset() {
    if (this.widgetId !== null) {
      turnstile.reset(this.widgetId)
    }
  }

  remove() {
    if (this.widgetId !== null) {
      turnstile.remove(this.widgetId)
      this.widgetId = null
    }
  }

  getToken(): string | undefined {
    if (this.widgetId === null) return undefined
    return turnstile.getResponse(this.widgetId)
  }
}

// Usage
const manager = new TurnstileManager(SITE_KEY)
manager.render('#container', {
  onSuccess: (token) => console.log('Token:', token),
  onError: (error) => console.error('Error:', error),
})

使用する場面:プログラムからのウィジェット制御が必要な SPA


同梱リソースの使用

スクリプト(scripts/)
  • check-csp.sh - Content Security Policy で Turnstile のスクリプトと iframe が許可されているかを確認します

使用例:

./scripts/check-csp.sh https://example.com
リファレンス(references/)
  • references/widget-configs.md - すべてのウィジェット設定オプションの完全なリファレンス
  • references/error-codes.md - トラブルシューティングを含む、エラーコードの包括的なリファレンス
  • references/testing-guide.md - テスト戦略、ダミーキー、E2E のパターン
  • references/react-integration.md - React 固有のパターンと @marsidev/react-turnstile の使用方法

Claude がこれらを読み込むタイミング:

  • widget-configs.md:ウィジェットの外観、テーマ、実行モードを設定するとき
  • error-codes.md:エラーコード 100*、200*、300*、400*、600* をデバッグするとき
  • testing-guide.md:E2E テストまたはローカル開発環境を設定するとき
  • react-integration.md:React、Next.js との連携時、または React 固有の問題が発生したとき
テンプレート(templates/)
  • wrangler-turnstile-config.jsonc - Cloudflare Workers 環境設定
  • turnstile-widget-implicit.html - 暗黙的レンダリングの HTML 例
  • turnstile-widget-explicit.ts - 明示的レンダリングの JavaScript API
  • turnstile-server-validation.ts - Siteverify API 検証関数
  • turnstile-react-component.tsx - @marsidev/react-turnstile を使用する React コンポーネント
  • turnstile-hono-route.ts - 検証を行う Hono ルートハンドラー
  • turnstile-test-config.ts - ダミーキーを使用するテスト設定

高度なトピック

SPA 向けの事前クリアランス

Turnstile は、シングルページアプリケーションでページ遷移後も維持される事前クリアランス Cookie を発行できます。

turnstile.render('#container', {
  sitekey: SITE_KEY,
  callback: async (token) => {
    // Request pre-clearance cookie
    await fetch('/api/pre-clearance', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ token }),
    })
  },
})
カスタムアクションと cData

異なるチャレンジの種類を追跡したり、カスタムデータを渡したりできます。

turnstile.render('#container', {
  sitekey: SITE_KEY,
  action: 'login', // Track action in analytics
  cdata: JSON.stringify({ userId: '123' }), // Custom data (max 255 chars)
  callback: (token) => {
    // Token includes action and cdata for server validation
  },
})

サーバー側の検証:

const result = await validateTurnstile(token, secretKey)

if (result.action !== 'login') {
  return new Response('Invalid action', { status: 400 })
}

const customData = JSON.parse(result.cdata || '{}')
再試行とエラー処理の戦略
class TurnstileWithRetry {
  private retryCount = 0
  private maxRetries = 3

  render(containerId: string) {
    turnstile.render(containerId, {
      sitekey: SITE_KEY,
      retry: 'auto', // or 'never' for manual control
      'retry-interval': 8000, // ms between retries
      'error-callback': (error) => {
        this.handleError(error)
      },
    })
  }

  private handleError(error: string) {
    // Error codes that should not retry
    const noRetry = ['110100', '110200', '110500']

    if (noRetry.some(code => error.includes(code))) {
      this.showFallback()
      return
    }

    // Retry on transient errors
    if (this.retryCount < this.maxRetries) {
      this.retryCount++
      setTimeout(() => {
        turnstile.reset(this.widgetId)
      }, 2000 * this.retryCount) // Exponential backoff
    } else {
      this.showFallback()
    }
  }

  private showFallback() {
    // Show alternative verification method
    console.error('Turnstile failed - showing fallback')
  }
}
複数のウィジェットがあるページ
const widgets = {
  login: null as string | null,
  signup: null as string | null,
}

// Render multiple widgets
widgets.login = turnstile.render('#login-widget', {
  sitekey: SITE_KEY,
  action: 'login',
})

widgets.signup = turnstile.render('#signup-widget', {
  sitekey: SITE_KEY,
  action: 'signup',
})

// Reset specific widget
turnstile.reset(widgets.login)

// Get token from specific widget
const loginToken = turnstile.getResponse(widgets.login)

依存関係

必須:

  • なし(Turnstile は CDN から読み込まれます)

オプション(React):

オプション(その他のフレームワーク):

  • vue-turnstile - Vue 3 連携
  • cfturnstile-vue3 - Vue 3 向けの代替ラッパー
  • ngx-turnstile - Angular 連携
  • svelte-turnstile - Svelte 連携
  • @nuxtjs/turnstile - Nuxt のフルスタック連携

公式ドキュメント


パッケージのバージョン(2025-10-22 に確認済み)

{
  "devDependencies": {
    "@marsidev/react-turnstile": "^1.3.1",
    "turnstile-types": "^1.2.3"
  }
}

注記:

  • @marsidev/react-turnstile は、Cloudflare が推奨する React パッケージです
  • 2025年9月に最終更新(積極的にメンテナンスされています)
  • React 18 以降、Next.js 13 以降、Next.js 14 以降、Next.js 15 以降に対応しています

本番環境での実装例

このスキルは、本番環境での実装に基づいています。

  • Cloudflare Workers: 公式 HTMLRewriter の例
  • React アプリ: @marsidev/react-turnstile(Cloudflare による検証済み)
  • コミュニティ: WordPress、Craft CMS、SilverStripe、Statamic との統合
  • 検証: ✅ 既知の12件の問題をすべて記録し、発生を防止しています

トラブルシューティング

問題:エラー 110200 - 「不明なドメイン」

解決策:Cloudflare ダッシュボードで、ウィジェットの許可されたドメインに自分のドメイン(開発用の localhost を含む)を追加してください。ローカル開発では、代わりにダミーのテスト sitekey 1x00000000000000000000AA を使用してください。

問題:エラー 300030 - 正規ユーザーのウィジェットがクラッシュする

解決策:再試行ロジックを備えたエラーコールバックを実装してください。これは Cloudflare 側で確認されている問題です(2025年)。再試行に失敗した場合は、別の検証方法にフォールバックしてください。

問題:トークンが常に success: false を返す

解決策:

  1. トークンの有効期限が切れていないことを確認してください(TTL は5分です)
  2. シークレットキーが正しいことを確認してください
  3. トークンがすでに検証されていないことを確認してください(トークンは1回のみ使用できます)
  4. ホスト名がウィジェットの設定と一致することを確認してください
問題:CSP によって iframe がブロックされる(エラー 200500)

解決策:次の CSP ディレクティブを追加してください。

<meta http-equiv="Content-Security-Policy" content="
  frame-src https://challenges.cloudflare.com;
  script-src https://challenges.cloudflare.com;
">
問題:Safari 18 の「IP アドレスを非公開にする」によってエラー 300010 が発生する

解決策:Safari の「IP アドレスを非公開にする」設定を無効にするよう、エラーメッセージに記載してください(Safari → 設定 → プライバシー → IP アドレスを非公開にする → オフ)

問題:Next.js と Jest のテストが @marsidev/react-turnstile で失敗する

解決策:Jest のセットアップで Turnstile コンポーネントをモックしてください。

// jest.setup.ts
jest.mock('@marsidev/react-turnstile', () => ({
  Turnstile: () => <div data-testid="turnstile-mock" />,
}))

完全セットアップチェックリスト

このチェックリストを使って、セットアップを確認してください。

  • Cloudflare ダッシュボードで Turnstile ウィジェットを作成した
  • 許可するドメインを追加した(開発用の localhost を含む)
  • フロントエンドのウィジェットが https://challenges.cloudflare.com/turnstile/v0/api.js から読み込まれる
  • 正しいサイトキーでウィジェットがレンダリングされる
  • エラーコールバックを実装し、テストした
  • サーバーサイドの Siteverify 検証を実装した
  • シークレットキーを環境変数に保存した(ハードコードしていない)
  • トークンの検証に remoteip チェックが含まれている
  • CSP を使用している場合、challenges.cloudflare.com を許可している
  • テストでダミーのサイトキー(1x00000000000000000000AA)を使用している
  • トークンの有効期限処理を実装した(TTL は 5 分)
  • ウィジェットのアクセシビリティをテストした(キーボード操作、スクリーンリーダー)
  • エラー状態でユーザーにわかりやすいメッセージを表示する
  • 本番環境へのデプロイでは、開発環境・ステージング環境とは別のウィジェットを使用する

質問や問題がある場合

  1. 個別のエラーのトラブルシューティングについては、references/error-codes.md を確認してください
  2. 3 ステップのセットアッププロセスのすべての手順を確認してください
  3. 公式ドキュメントを確認してください:https://developers.cloudflare.com/turnstile/
  4. サーバーサイドの検証が実装されていることを確認してください(最もよくある問題です)
  5. Cloudflare Docs MCP ツールを使用してください:mcp__cloudflare-docs__search_cloudflare_documentation

トークン効率:約 65~70%削減(10~12k トークン → 3~4k トークン)

防止できるエラー:解決策を含む、文書化された 12 件の問題

コメント