cloudflare-turnstile について
cloudflare-turnstileは、WebサイトにCloudflare Turnstileを導入する開発者向けの実装・トラブルシューティングスキルです。クライアントウィジェット、サーバー側のトークン検証、React連携、テスト、一般的なエラーを扱います。
主な機能
- 01Turnstileウィジェットの設定ガイド
- 02クライアント側の表示とトークン処理
- 03サーバー側のSiteverify検証
- 04ReactとNext.jsの連携パターン
- 05エンドツーエンドテストのガイド
- 06エラーコードとトラブルシューティング資料
活用シーン
- 01フォームへのボット検証追加
- 02Workers APIルートでのトークン検証
- 03ReactフォームへのTurnstile導入
- 04テストキーを使ったエンドツーエンドテストの設定
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ secondsky/ claude-skills --skill cloudflare-turnstileインストール後、Claude Code で/cloudflare-turnstileを入力して読み込まれたか確認します。
同梱ファイル
SKILL.md と一緒にインストールされる参考資料やスクリプト。
references/advanced-topics.mdbrowser-support.mdcommon-patterns.mderror-codes.mdmigration-guide.mdmobile-implementation.mdreact-integration.mdsetup-checklist.mdtesting-guide.mdwidget-configs.mdscripts/check-csp.shtemplates/turnstile-hono-route.tsturnstile-react-component.tsxturnstile-server-validation.tsturnstile-test-config.tsturnstile-widget-explicit.tsturnstile-widget-implicit.htmlwrangler-turnstile-config.jsonc詳細
Cloudflare Turnstile
ステータス:本番環境で利用可能 ✅ | 最終確認日:2025-11-26
依存関係:なし(オプション:React 用の @marsidev/react-turnstile)
目次:クイックスタート • 重要なルール • 上位 12 件のエラー • 一般的なパターン • リファレンスを読み込むタイミング • トラブルシューティング
クイックスタート(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:ウィジェットの設定を作成する
- Cloudflare ダッシュボードにログインします
- Turnstile セクションに移動します
- 「Add Site」をクリックします
- 以下を設定します:
- ウィジェットモード: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 を使ったサーバー側の検証を必須としている
課題 #2:トークンの有効期限(5分)
エラー:有効なトークンを時間をおいて送信すると success: false になる
参照元:https://developers.cloudflare.com/turnstile/get-started/server-side-validation
原因:トークンは生成から300秒後に期限切れになる
予防策:テンプレートで有効期限を説明し、期限切れ時にトークンを更新する仕組みを実装している
課題 #3:シークレットキーがフロントエンドに露出する
エラー:セキュリティが回避され、攻撃者が自分のトークンを検証できる 参照元:https://developers.cloudflare.com/turnstile/get-started/server-side-validation 原因:シークレットキーが JavaScript にハードコードされている、またはソース内から見える状態になっている 予防策:すべてのテンプレートで、環境変数を使ったバックエンドのみでの検証を示している
課題 #4:Siteverify への GET リクエスト
エラー:API が 405 Method Not Allowed を返す 参照元:https://developers.cloudflare.com/turnstile/migration/recaptcha 原因:reCAPTCHA は GET をサポートするが、Turnstile では POST が必須 予防策:テンプレートでは FormData または JSON ボディを使った POST を使用している
課題 #5:コンテンツセキュリティポリシーによるブロック
エラー:エラー 200500 - 「読み込みエラー:iframe を読み込めませんでした」 参照元:https://developers.cloudflare.com/turnstile/troubleshooting/client-side-errors/error-codes 原因:CSP が challenges.cloudflare.com の iframe をブロックしている 予防策:スキルに CSP 設定の参考情報と check-csp.sh スクリプトを含めている
課題 #6:ウィジェットのクラッシュ(エラー 300030)
エラー:正規ユーザーに対して汎用的なクライアント実行エラーが発生する 参照元:https://community.cloudflare.com/t/turnstile-is-frequently-generating-300x-errors/700903 原因:不明 — Cloudflare 側の問題と思われる(2025年) 予防策:テンプレートでエラーコールバック、リトライロジック、フォールバック処理を実装している
課題 #7:設定エラー(エラー 600010)
エラー:「設定エラー」によりウィジェットが失敗する 参照元:https://community.cloudflare.com/t/repeated-cloudflare-turnstile-error-600010/644578 原因:ウィジェット設定にホスト名がない、または削除されている 予防策:テンプレートでホスト名の許可リストが必要であることと、確認手順を説明している
課題 #8:Safari 18/macOS 15 の「IP アドレスを非公開」問題
エラー:Safari の「IP アドレスを非公開」が有効な場合にエラー 300010 が発生する 参照元:https://community.cloudflare.com/t/turnstile-is-frequently-generating-300x-errors/700903 原因:プライバシー設定がチャレンジのシグナルを妨げている 予防策:エラー処理の参考情報で Safari の回避策(「IP アドレスを非公開」を無効にする)を説明している
課題 #9:Brave ブラウザーで紙吹雪アニメーションが失敗する
エラー:成功アニメーション中に検証が失敗する 参照元:https://github.com/brave/brave-browser/issues/45608(2025年4月) 原因:Brave のシールドがアニメーションスクリプトをブロックしている 予防策:テンプレートでアニメーションが完了する前に成功を処理する
課題 #10:Next.js と Jest の非互換性
エラー:@marsidev/react-turnstile により Jest のテストが動作しなくなる 参照元:https://github.com/marsidev/react-turnstile/issues/112(2025年10月) 原因:Jest でのモジュール解決の問題 予防策:テストガイドに Jest のモックパターンとダミーの sitekey の使用方法を含めている
課題 #11:許可リストに localhost がない
エラー:エラー 110200 - 「不明なドメイン:このドメインは許可されていません」 参照元:https://developers.cloudflare.com/turnstile/troubleshooting/client-side-errors/error-codes 原因:許可リストに localhost がない状態で、開発環境に本番用ウィジェットを使用している 予防策:テンプレートでは開発時にダミーのテストキーを使用し、localhost を許可リストに追加する必要があることを説明している
Issue #12:トークンの再利用を試みる
エラー:success: false と「token already spent」エラー
参照元:https://developers.cloudflare.com/turnstile/troubleshooting/testing
発生する理由:各トークンは一度だけ検証できます
防止策:テンプレートには、トークンが一度しか使用できない制約とトークンを更新する方法が記載されています
設定
Wrangler(Workers):完全な設定には templates/wrangler-turnstile-config.jsonc を読み込んでください。主な設定項目:公開 sitekey 用の vars(コミットしても安全)と、非公開 secret key 用の secrets(wrangler secret put TURNSTILE_SECRET_KEY を使用)。
CSP ディレクティブ(Content Security Policy を使用する場合):
<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;">
よくあるパターン
Hono + Cloudflare Workers:Hono フレームワークを使用した Workers API ルートでのサーバー側検証。ボット対策が必要な Workers エンドポイントを作成する場合は、references/common-patterns.md #pattern-1 を読み込んでください。
React + Next.js:@marsidev/react-turnstile と連携するクライアント側フォーム。Turnstile を React/Next.js アプリケーションに統合する場合は、references/common-patterns.md #pattern-2 を読み込んでください。
E2E テスト:ダミーキーを使った自動テスト(Playwright、Cypress、Jest)。E2E テストを作成する場合や、CI/CD パイプラインを設定する場合は、references/common-patterns.md #pattern-3 を読み込んでください。
ウィジェットのライフサイクル:SPA 向けのプログラムによるウィジェット制御(render、reset、remove、getToken)。明示的なウィジェット管理が必要な SPA を作成する場合は、references/common-patterns.md #pattern-4 を読み込んでください。
リファレンスを読み込むタイミング
references/widget-configs.md:ウィジェットの外観、テーマ、実行モード、サイズ、言語、再試行動作を設定する場合。
references/error-codes.md:エラーコード 100*、200*、300*、400*、600* のデバッグや、クライアント側の障害(CSP、ドメインエラー、ウィジェットのクラッシュ)のトラブルシューティングを行う場合。
references/testing-guide.md:E2E テスト(Playwright、Cypress)の設定、ダミーキーを使ったローカル開発、CI/CD パイプラインとの統合を行う場合。
references/react-integration.md:React、Next.js との統合や、@marsidev/react-turnstile の問題(Jest のモック、SSR、フック)のトラブルシューティングを行う場合。
references/common-patterns.md:Hono Workers ルート、React フォーム、E2E テスト、またはウィジェットのライフサイクル管理(明示的なレンダリング)を実装する場合。
references/advanced-topics.md:SPA 向けの事前クリアランス、カスタムアクション/cdata、再試行戦略、または複数ウィジェットのページを実装する場合。
references/setup-checklist.md:デプロイの準備、設定の完了確認、本番環境への準備状況の確認を行う場合(14 項目のチェックリスト)。
references/migration-guide.md:reCAPTCHA(v2)または hCaptcha から Turnstile に移行する場合(互換モード、API の違い、Siteverify で POST のみが必須であることを含む)。
references/browser-support.md:ブラウザー互換性マトリックス、Safari 18 の「Hide IP」に関する回避策、Brave のシールドの問題、ブラウザー固有のフォールバックを確認する場合。
references/mobile-implementation.md:iOS、Android、React Native、Flutter 向けに WebView を統合する場合(User Agent の一貫性とストレージの永続化要件を含む)。
templates/:wrangler-turnstile-config.jsonc(Workers 環境)、turnstile-widget-implicit.html(静的フォーム)、turnstile-widget-explicit.ts(SPA レンダリング)、turnstile-server-validation.ts(Siteverify API)、turnstile-react-component.tsx(React との統合)、turnstile-hono-route.ts(Hono 検証)、turnstile-test-config.ts(テスト設定)
scripts/check-csp.sh:Content Security Policy で Turnstile が許可されていることを確認します(使用方法:./scripts/check-csp.sh https://example.com)
依存関係
必須:なし(Turnstile は Cloudflare CDN から読み込まれます)
オプション:@marsidev/[email protected](React)、[email protected](TypeScript)、vue-turnstile(Vue 3)、ngx-turnstile(Angular)、svelte-turnstile(Svelte)、@nuxtjs/turnstile(Nuxt)
公式ドキュメント
Turnstile: https://developers.cloudflare.com/turnstile/ • はじめに: https://developers.cloudflare.com/turnstile/get-started/ • エラーコード: https://developers.cloudflare.com/turnstile/troubleshooting/client-side-errors/error-codes/ • テスト: https://developers.cloudflare.com/turnstile/troubleshooting/testing/ • 移行(reCAPTCHA): https://developers.cloudflare.com/turnstile/migration/recaptcha/ • MCP: mcp__cloudflare-docs__search_cloudflare_documentation ツールを使用してください
トラブルシューティング
問題:エラー 110200 - 「不明なドメイン」
解決方法:Cloudflare ダッシュボードで、ウィジェットの許可済みドメインにドメイン(開発用の localhost を含む)を追加してください。ローカル開発では、代わりにダミーのテスト用サイトキー 1x00000000000000000000AA を使用してください。
問題:エラー 300030 - 正規のユーザーに対してウィジェットがクラッシュする
解決方法:再試行ロジックを含むエラーコールバックを実装してください。これは Cloudflare 側の既知の問題です(2025年)。再試行に失敗した場合は、別の検証方法にフォールバックしてください。
問題:トークンが常に success: false を返す
解決方法:
- トークンの有効期限(TTL が 5 分)が切れていないことを確認してください
- シークレットキーが正しいことを確認してください
- トークンがすでに検証されていないことを確認してください(1 回のみ使用可能)
- ホスト名がウィジェットの設定と一致していることを確認してください
問題: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" />,
}))
トークン効率:手動での統合と比べて約 65~70%削減
防止できるエラー:解決策を含む、文書化済みのセキュリティ/検証上の問題 12 件
デプロイチェックリスト:デプロイ前の 14 項目の完全な確認には references/setup-checklist.md を読み込んでください