cloudflare-turnstile について
cloudflare-turnstile は、Cloudflare Turnstile の導入方法をまとめたスキルです。フロントエンドのウィジェットとサーバー側のトークン検証を扱い、ウェブ開発者向けに統合、テスト、セキュリティ要件、トラブルシューティングを説明します。
主な機能
- 01暗黙的・明示的ウィジェットの設定
- 02React への Turnstile の統合
- 03Siteverify によるサーバー側トークン検証
- 04テストキーと検証パターンの利用
- 05一般的なエラーや CSP の問題の診断
活用シーン
- 01ウェブサイトのフォームへのボット対策追加
- 02API ルートでの Turnstile トークン検証
- 03検証成功・失敗シナリオのテスト
- 04ウィジェット読み込みや検証問題の診断
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ jezweb/ claude-skills --skill cloudflare-turnstileインストール後、Claude Code で/cloudflare-turnstileを入力して読み込まれたか確認します。
同梱ファイル
SKILL.md と一緒にインストールされる参考資料やスクリプト。
README.md.claude-plugin/plugin.jsonassets/example-template.txtreferences/error-codes.mdexample-reference.mdreact-integration.mdtesting-guide.mdwidget-configs.mdrules/cloudflare-turnstile.mdscripts/check-csp.shexample-script.shtemplates/turnstile-hono-route.tsturnstile-react-component.tsxturnstile-server-validation.tsturnstile-test-config.tsturnstile-widget-explicit.tsturnstile-widget-implicit.html詳細
Cloudflare Turnstile
ステータス:本番環境で利用可能 ✅ 最終更新日:2026-01-21 依存関係:なし(オプション:React 用の @marsidev/react-turnstile) 最新バージョン:@marsidev/[email protected]、[email protected]
最近の更新(2025):
- 2025 年 12 月:@marsidev/react-turnstile v1.4.1 で、スクリプト読み込み時の競合状態を修正
- 2025 年 8 月:v1.3.0 で、React のクロージャに関する問題に対応する
rerenderOnCallbackChangeプロパティを追加 - 2025 年 3 月:TopN 統計(7 つのディメンション:ホスト名、ブラウザー、国、ユーザーエージェント、ASN、OS、送信元 IP)を含む Turnstile Analytics のアップグレード、異常検知、ボットの挙動監視を強化
- 2025 年 1 月:remoteip の検証を一時的に強制(解決済み。ただし、正しい IP の渡し方が重要であることを示しています)
- 2025 年:WCAG 2.1 AA 準拠、Free プラン(ウィジェット 20 個、分析データ 7 日分)、Enterprise 機能(ウィジェット数無制限、エフェメラル ID、すべてのホスト名に対応、分析データ 30 日分、オフラベルブランディング)
クイックスタート(5 分)
# 1. Create widget: https://dash.cloudflare.com/?to=/:account/turnstile
# Copy sitekey (public) and secret key (private)
# 2. Add widget to frontend
<script src="https://challenges.cloudflare.com/turnstile/v0/api.js" async defer></script>
<form>
<div class="cf-turnstile" data-sitekey="YOUR_SITE_KEY"></div>
<button type="submit">Submit</button>
</form>
# 3. Validate token server-side (Cloudflare Workers)
const formData = await request.formData()
const token = formData.get('cf-turnstile-response')
const verifyFormData = new FormData()
verifyFormData.append('secret', env.TURNSTILE_SECRET_KEY)
verifyFormData.append('response', token)
verifyFormData.append('remoteip', request.headers.get('CF-Connecting-IP')) // REQUIRED - see Critical Rules
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', { status: 401 })
重要:
- トークンの有効期限は 5 分で、1 回のみ使用できます
- 必ずサーバー側で検証してください(Siteverify API が必須です)
- api.js をプロキシ/キャッシュしないでください(Cloudflare CDN から読み込む必要があります)
- 開発/ステージング/本番環境では、異なるウィジェットを使用してください
レンダリングモード
暗黙的(ページ読み込み時に自動レンダリング):
<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"></div>
明示的(SPA 向けのプログラムによる制御):
<script src="https://challenges.cloudflare.com/turnstile/v0/api.js?render=explicit"></script>
const widgetId = turnstile.render('#container', { sitekey: 'YOUR_SITE_KEY' })
turnstile.reset(widgetId) // Reset widget
turnstile.getResponse(widgetId) // Get token
React(@marsidev/react-turnstile を使用):
import { Turnstile } from '@marsidev/react-turnstile'
<Turnstile siteKey={TURNSTILE_SITE_KEY} onSuccess={setToken} />
重要なルール
必ず行うこと
✅ Siteverify API を呼び出す - サーバー側での検証は必須です
✅ HTTPS を使用する - HTTP 経由で検証しないでください
✅ シークレットキーを保護する - フロントエンドのコードで公開しないでください
✅ トークンの有効期限を処理する - トークンは 5 分後に期限切れになります
✅ エラーコールバックを実装する - 失敗を適切に処理してください
✅ テストにはダミーキーを使用する - テスト用 sitekey:1x00000000000000000000AA
✅ 妥当なタイムアウトを設定する - 検証を無期限に待たないでください
✅ action/hostname を検証する - 指定されている場合は追加フィールドを確認してください
✅ キーを定期的にローテーションする - ダッシュボードまたは API を使用してシークレットをローテーションしてください
✅ 分析データを監視する - 解決率と失敗を追跡してください
✅ クライアント IP を必ず Siteverify に渡す - CF-Connecting-IP ヘッダー(Workers)または X-Forwarded-For(Node.js)を使用してください。Cloudflare は 2025 年 1 月に remoteip の厳格な検証を一時的に強制し、正しい IP を渡していないサイトで広範囲に障害が発生しました
絶対に行わないこと
❌ サーバー側の検証を省略する - クライアント側だけの検証はセキュリティ上の脆弱性になります ❌ api.js スクリプトをプロキシする - Cloudflare CDN から読み込む必要があります ❌ トークンを再利用する - 各トークンは 1 回のみ使用できます ❌ GET リクエストを使用する - Siteverify が受け付けるのは POST のみです ❌ シークレットキーを公開する - シークレットはバックエンド環境内だけに保管してください ❌ クライアント側の検証を信用する - トークンは偽造される可能性があります ❌ api.js をキャッシュする - 将来の更新によって統合が機能しなくなる可能性があります ❌ テストで本番環境のキーを使用する - 代わりにダミーキーを使用してください ❌ エラーコールバックを無視する - 必ず失敗を処理してください
既知の問題の防止
このスキルは、文書化された 15 件の問題を防止します:
問題 #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 秒後に期限切れになるため
防止策:テンプレートには TTL が記載されており、期限切れ時のトークン更新が実装されています
問題 #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 — 「Loading error: The iframe could not be loaded」 出典: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)
エラー:ウィジェットで「configuration error」が発生します 出典: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 — 「Unknown domain: Domain not allowed」 出典:https://developers.cloudflare.com/turnstile/troubleshooting/client-side-errors/error-codes 発生する理由:許可リストに localhost がないまま、開発環境で本番用ウィジェットを使用しています 防止策:テンプレートでは、開発時にダミーのテストキーを使用し、localhost を許可リストに追加する要件を説明しています
問題 #12:トークンの再利用を試みる
エラー:success: false と「token already spent」エラーが発生します
出典:https://developers.cloudflare.com/turnstile/troubleshooting/testing
発生する理由:各トークンは一度しか検証できません。Turnstile のトークンは使い捨てです。検証後(成功または失敗を問わず)、トークンは消費され、再検証できません。後続の送信で新しいトークンを生成するには、開発者が明示的に turnstile.reset() を呼び出す必要があります。
防止策:テンプレートでは、使い捨ての制約とトークンを更新するパターンを説明しています
// CRITICAL: Reset widget after validation to get new token
const turnstileRef = useRef(null)
async function handleSubmit(e) {
e.preventDefault()
const token = formData.get('cf-turnstile-response')
const result = await fetch('/api/submit', {
method: 'POST',
body: JSON.stringify({ token })
})
// Reset widget regardless of success/failure
// Token is consumed either way
if (turnstileRef.current) {
turnstile.reset(turnstileRef.current)
}
}
<Turnstile
ref={turnstileRef}
siteKey={TURNSTILE_SITE_KEY}
onSuccess={setToken}
/>
Issue #13:エラー 106010 - Chrome/Edge で初回読み込みに失敗
エラー:Chrome/Edge ブラウザーでウィジェットを初めて読み込む際に、106010 - 「一般的なパラメーターエラー」が発生します
参照元:Cloudflare エラーコード、コミュニティの報告
発生する理由:Chrome および Edge で、ページの初回読み込み時に影響するブラウザー固有の問題です。コンソールには https://challenges.cloudflare.com/cdn-cgi/challenge-platform への 400 エラーが表示されます。Firefox では発生しません。ページを再読み込みすると正常に動作します。
予防策:エラーコールバックに自動再試行ロジックを実装します
turnstile.render('#container', {
sitekey: SITE_KEY,
retry: 'auto',
'retry-interval': 8000,
'error-callback': (errorCode) => {
if (errorCode === '106010') {
console.warn('Chrome/Edge first-load issue (106010), auto-retrying...')
// Auto-retry will handle it
}
}
})
回避策:ページを再読み込みすると、ウィジェットは正常に動作します。自動再試行の設定で、ほとんどの場合は解決します。ブラウザー拡張機能が原因でないことを確認するには、シークレットモードでテストします。Cloudflare Turnstile のエンドポイントが許可されていることを確認するため、CSP ルールを確認します。
Issue #14:複数ウィジェットの表示ステータスが更新されない(コミュニティ提供)
エラー:トークンの生成に成功しても、ウィジェットに「保留中...」というステータスが表示されます
参照元:GitHub Issue #119
発生する理由:1 つのページで複数の <Turnstile/> コンポーネントをレンダリングする際に発生する、CSS の再描画に関する問題です。フル HD のデスクトップ画面でのみ再現します。トークンは正常に生成されており(検証は機能します)が、表示上のステータスが更新されません。ウィジェットにマウスカーソルを合わせると再描画が行われ、正しいステータスが表示されます。
予防策:成功コールバックで CSS の再描画を強制します
<Turnstile
siteKey={KEY}
onSuccess={(token) => {
setToken(token)
// Force repaint by toggling display
const widget = document.querySelector('.cf-turnstile')
if (widget) {
widget.style.display = 'none'
setTimeout(() => widget.style.display = 'block', 0)
}
}}
/>
注:これは表示上のみの問題であり、検証の失敗ではありません。トークンは正しく生成され、機能します。
Issue #15:@marsidev/react-turnstile の Jest 互換性(2025 年 12 月更新)
エラー:@marsidev/react-turnstile のインポート時に Jest encountered an unexpected token が発生します
参照元:GitHub Issue #114、GitHub Issue #112
発生する理由:Jest 30.2.0(2025 年 12 月時点の最新版)で、ESM モジュールの解決に関する問題が発生します。Issue #112 はメンテナーによって「対応予定なし」としてクローズされました。Jest ユーザーは身動きが取れない状態ですが、Vitest への移行で解決できます。
予防策:Jest のセットアップで Turnstile コンポーネントをモックするか、Vitest に移行します
// Option 1: Jest mocking (jest.setup.ts)
jest.mock('@marsidev/react-turnstile', () => ({
Turnstile: () => <div data-testid="turnstile-mock" />,
}))
// Option 2: transformIgnorePatterns in jest.config.js
module.exports = {
transformIgnorePatterns: [
'node_modules/(?!(@marsidev/react-turnstile)/)'
]
}
// Option 3 (Recommended): Migrate to Vitest
// Vitest handles ESM modules correctly without mocking
ステータス:メンテナーはこの Issue を「対応予定なし」としてクローズしました。新しいプロジェクトでは Vitest への移行をおすすめします。
設定
wrangler.jsonc:
{
"vars": { "TURNSTILE_SITE_KEY": "1x00000000000000000000AA" },
"secrets": ["TURNSTILE_SECRET_KEY"] // Run: wrangler secret put TURNSTILE_SECRET_KEY
}
必須の CSP:
<meta http-equiv="Content-Security-Policy" content="
script-src 'self' https://challenges.cloudflare.com;
frame-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') || '') // CRITICAL - always pass client 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 のクライアント側フォーム
テストキー
ダミーサイトキー(クライアント):
- 常に成功:
1x00000000000000000000AA - 常にブロック:
2x00000000000000000000AB - インタラクティブを強制:
3x00000000000000000000FF
ダミーシークレットキー(サーバー):
- 常に成功:
1x0000000000000000000000000000000AA - 常に失敗:
2x0000000000000000000000000000000AA - トークンは使用済み:
3x0000000000000000000000000000000AA
同梱リソース
スクリプト: check-csp.sh - CSP で Turnstile が許可されていることを確認します
参照資料:
widget-configs.md- すべての設定オプションerror-codes.md- エラーコードのトラブルシューティング(100*/200*/300*/400*/600*)testing-guide.md- テスト戦略、ダミーキーreact-integration.md- React/Next.js のパターン
テンプレート: Hono、React、暗黙的/明示的なレンダリング、検証の完全なサンプル
高度な機能
事前クリアランス(SPA): ページ遷移後も維持される Cookie を発行します
turnstile.render('#container', {
sitekey: SITE_KEY,
callback: async (token) => {
await fetch('/api/pre-clearance', { method: 'POST', body: JSON.stringify({ token }) })
}
})
カスタムアクションとデータ: チャレンジの種類を追跡し、カスタムデータを渡します(最大 255 文字)
turnstile.render('#container', {
action: 'login', // Track in analytics
cdata: JSON.stringify({ userId: '123' }), // Custom payload
})
エラー処理: 信頼性を高めるために retry: 'auto' と error-callback を使用します
turnstile.render('#container', {
retry: 'auto',
'retry-interval': 8000, // ms between retries
'error-callback': (error) => { /* handle or show fallback */ }
})
依存関係
必須: なし(CDN から読み込みます) React: @marsidev/[email protected](Cloudflare 推奨)、[email protected] その他: vue-turnstile、ngx-turnstile、svelte-turnstile、@nuxtjs/turnstile
公式ドキュメント
- https://developers.cloudflare.com/turnstile/
mcp__cloudflare-docs__search_cloudflare_documentationツールを使用します
トラブルシューティング
問題:エラー 110200 - 「Unknown domain」
解決策:Cloudflare ダッシュボードで、ウィジェットの許可ドメインに使用するドメイン(開発用の localhost を含む)を追加します。ローカル開発では、代わりにダミーのテスト sitekey 1x00000000000000000000AA を使用します。
問題:エラー 300030 - 正規ユーザーに対してウィジェットがクラッシュする
解決策:再試行ロジックを含むエラーコールバックを実装します。これは Cloudflare 側で既知の問題です(2025 年)。再試行に失敗した場合は、別の検証方法にフォールバックします。
問題:トークンが常に success: false を返す
解決策:
- トークンの有効期限が切れていないことを確認します(TTL は 5 分です)
- シークレットキーが正しいことを確認します
- トークンが以前に検証されていないことを確認します(使い捨てです)
- ホスト名がウィジェットの設定と一致することを確認します
問題: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 コンポーネントをモックするか、ESM モジュールを正しく処理できる Vitest に移行します:
// Option 1: Jest mocking (jest.setup.ts)
jest.mock('@marsidev/react-turnstile', () => ({
Turnstile: () => <div data-testid="turnstile-mock" />,
}))
// Option 2: Migrate to Vitest (recommended for new projects)
// Vitest handles ESM modules without mocking required
注記:この問題は Jest 30.2.0 でも解消されていません(2025 年 12 月)。メンテナーは「対応予定なし」としてクローズしました。詳細については Issue #15 を参照してください。
防止できるエラー: ドキュメント化された 15 件の問題(Safari 18 の「IP アドレスを非公開にする」、Brave の紙吹雪、Next.js Jest、CSP によるブロック、トークンの再利用、期限切れ、ホスト名の許可リスト、ウィジェットのクラッシュ 300030、設定エラー 600010、検証の欠落、GET リクエスト、シークレットの露出、Chrome/Edge 106010、複数ウィジェットのレンダリング、トークン再生成のパターン)