LaravelにCloudflare Turnstileを実装してみた3

2025.12.16 10:00
2025.12.18 12:25
LaravelにCloudflare Turnstileを実装してみた3

前回までで、Turnstile を Laravel(カスタムパッケージ)に組み込むところまではできました。
とはいえ、実際に運用していくと「ローカルでどう確認する?」「CIで詰まらせないには?」「Featureテストではどこまで見る?」みたいな話が地味に効いてきます。

Turnstile は便利なんですが、Bot対策という性質上「外部サービスに依存している」ので、何も考えずにテストを書き始めると、たまに落ちる・環境で挙動が変わるみたいな事故が起きがちです。

そこで今回は、Cloudflare が用意している テストキーの使い分けと、
Featureテスト/CIでの扱い方を、実務メモとしてまとめます。

1. ローカル環境・テスト環境でのTurnstile動作確認

Turnstile はありがたいことに、Cloudflare 公式が テストキーを用意してくれています。
これを使うと「本番キーを持っていない環境」でも、かなり本番に近い形で確認できます。

ここでの考え方としては、

  • 普段の開発は「常に成功」キーで固定
  • エラー確認のときだけ「常に失敗」キーに切り替え
  • UX確認が必要なときだけ「チャレンジ強制」キーを使う

という運用が一番ラクでした。

1-1. 常に成功するテストキー(推奨)

TURNSTILE_SITE_KEY=1x00000000000000000000AA
TURNSTILE_SECRET_KEY=1x0000000000000000000000000000000AA

挙動:

  • ウィジェットは通常どおり表示される
  • バリデーションは常に success = true
  • ローカル環境やCIでも安定して通る

使い所:

  • 通常のローカル開発
  • CI/CDでの自動テスト
  • フロントエンドの見た目確認

「基本はこれだけでいい」と思ってます。
Bot対策の動作確認が必要な場面でも、まずはこのキーが安定です。

フォーム送信時に cf-turnstile-response が送信されていれば成功します。

$rules['cf-turnstile-response'] = ['required', new Turnstile()];

この状態で:

  • ローカル環境
  • ステージング
  • Featureテスト(HTTP実通信なし)

すべて問題なく通ります。

1-2. 常に失敗するテストキー

TURNSTILE_SITE_KEY=2x00000000000000000000AB
TURNSTILE_SECRET_KEY=2x0000000000000000000000000000000AB

挙動:

  • ウィジェットは表示される
  • バリデーションは常に success = false
  • エラーハンドリングをテストできる

使い所:

  • エラー表示のテスト
  • バリデーション失敗時の挙動確認
  • エラーメッセージのデザイン確認

普段は使いません。
失敗したときにどうなるか」を確認したい時だけ、短時間で切り替える用途です。

また、明示的に Turnstile の検証失敗を確認したい場合は、
不正なレスポンストークンを送る方法でもいけます。

例:POSTデータに空文字を送る

$response = $this->post('/form/submit', [
    'name' => 'テスト',
    'email' => 'test@example.com',
    'cf-turnstile-response' => '',
]);

結果:

  • required に引っかかる
  • バリデーションエラーとしてフォームに戻る

ここは Turnstile の前に Laravel 側の required で止まるので、
「UIのエラー表示確認」には分かりやすいです。

例:ランダム文字列を送る

'cf-turnstile-response' => 'invalid-token',

この場合、required は通りますが、
Turnstile Rule 内で success = false となり、

$fail('認証に失敗しました。もう一度お試しください。');

が返ります。

1-3. チャレンジを強制するテストキー

TURNSTILE_SITE_KEY=3x00000000000000000000FF
TURNSTILE_SECRET_KEY=3x0000000000000000000000000000000FF

挙動:

  • ウィジェットが実際のチャレンジ(インタラクション)を表示
  • 本番環境に近い動作を確認できる
  • ユーザーが「確認」ボタンをクリックする必要がある

使い所:

  • 本番に近いUX確認
  • チャレンジ表示時のレイアウト確認
  • ユーザー操作フローのテスト

Turnstile では、状況によって 追加チャレンジ(Challenge) が発生します。
ただし、これは以下の理由から ローカルでの再現はほぼ不要 と判断しました。
理由は以下です。

  • チャレンジの有無は Cloudflare 側の判定
  • フロントエンドで完結し、サーバー側は結果のみ受け取る
  • サーバー側では success = true / false しか見ない

そのため、

チャレンジが出ても、最終的に成功すれば success = true

という前提で設計しています。

つまりこのキーは、「サーバー側のテスト」ではなく UX確認用ですね。

まとめ

キー結果主な用途
1x...AA常に成功通常の開発・CI
2x...AB常に失敗エラー処理のテスト
3x...FFチャレンジ表示本番環境に近い動作確認

基本は 1x...AA(常に成功)でOKです。エラー処理を確認したいときだけ 2x...AB に切り替える、という使い方がおすすめです!

2. Featureテストでの扱い

結論から言うと、Featureテストでは
Turnstile 自体を検証対象にしないのがおすすめです。

理由は単純で、

  • Turnstile の正しさは Cloudflare 側の責務
  • 自分たちが担保したいのは「フォームが正しく動くこと」
  • 外部通信が入るとテストが不安定になりがち

だからです。

2-1. 方法①:Turnstileを無効化する(おすすめ)

テスト環境(testing)では、
Turnstile のルールを追加しないようにします。

if (app()->environment('testing')) {
    return $rules;
}

運用的にはこれが一番ラクでした。
「テストが外部要因で落ちる」事故を避けられます。

2-2. 方法②:ダミートークンを送る

フォーム送信の形だけ揃えたい場合は、
テスト時にダミートークンを入れます。

$response = $this->post('/form/submit', [
    // フォーム項目
    'cf-turnstile-response' => 'test-token',
]);

そのうえで、テスト環境では Turnstile Rule をスキップします。
(「フォーム送信の形」を本番と近づけたい時に便利です)

CIで詰まらないためのポイント

CI で詰まらせないために、個人的に意識したのはこの3つです。

  • CI 環境では 必ずテストキーを設定
  • 本番キーを .env.testing に入れない
  • 外部通信に依存しない設計にする

.env.testing には、常に成功キーを入れておくのが安定です。

# .env.testing
TURNSTILE_SITE_KEY=1x00000000000000000000AA
TURNSTILE_SECRET_KEY=1x0000000000000000000000000000000AA

これで GitHub Actions 等でも安定します。

まとめ(テストまわり)

  • 通常は 常に成功するテストキーで十分
  • 失敗系は cf-turnstile-response を壊すだけで確認可能
  • チャレンジはサーバー側では気にしなくてよい(UX確認用)
  • Featureテストでは Turnstile を責務外にするのが安全

Turnstile は
「テストで詰まらない Bot 対策」
という点でも、かなり扱いやすい印象でした。

まとめ

  • カスタムパッケージ内でも問題なく導入できた
  • Rule化+Bladeコンポーネントで責務分離しやすい
  • reCAPTCHAよりUXがかなり軽い

「とりあえずBotを弾きたい」用途なら、Turnstile はかなり扱いやすい印象でした。
これで Turnstile シリーズはひとまず終了です。

今回は以上です!