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

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

これまでお問い合わせフォームの Bot 対策として
Google reCAPTCHA を利用していました。

ただ最近、reCAPTCHA の管理画面や公式ドキュメントを見ていると、
reCAPTCHA Enterprise への移行を前提とした案内や導線がかなり増えてきた
という印象を受けるようになりました。

実際、WordPress の Contact Form 7 でも注意喚起されている通り、
Google は reCAPTCHA を reCAPTCHA Enterprise へ
段階的に移行させていく方針を示しています。

現時点ですべてのユーザーが
ただちに Enterprise へ強制移行される、という状況ではありません。
ただ、Enterprise では Google Cloud プロジェクトの作成や
クレジットカード登録が前提となるため、
特に小規模サイトやクライアント案件では
導入・運用のハードルが一気に上がると感じました。

仕様上は、
reCAPTCHA v2 / v3 をカード登録なしで使い続けられるルートも残っています。
ただ、管理画面や公式ドキュメントの流れを見る限り、
その状態を維持するには
ややハック的な運用を続ける必要がありそう、
というのが正直な感想です。

「今すぐ困っているわけではないが、
このまま使い続けるのは将来リスクが読みにくい」

そう判断し、
将来的なコスト増や運用リスクを避ける目的で、
今回は Cloudflare Turnstile へ移行しました。


今回は、
Laravel のカスタムパッケージ(DDD構成)内で Cloudflare Turnstile を実装したときの手順を、 あとで自分が見返せるようにメモとして残します。


環境

  • Laravel 12
  • カスタムパッケージ構成(DDD寄り)
  • Cloudflare Turnstile

※ 新規フォームではなく、既存のお問い合わせフォームへの後付け実装です。

1. CloudflareでTurnstileのキーを取得

まずは Cloudflare のダッシュボードで
Turnstile を追加し、サイトキーとシークレットキーを取得します。

キーの取得方法はこちらの記事でくわしく説明しています。

取得したキーは、Laravel 側では .env に定義します。

TURNSTILE_SITE_KEY=XXXXXXXXXXXXX
TURNSTILE_SECRET_KEY=XXXXXXXXXXXXX

この時点では、
Laravel 側で特別な設定やコード追加はまだ行いません。

2. 設定ファイルを作成

次に、Turnstile 用の設定を
/laravelルートDIR/config/turnstile.php に切り出します。

<?php

return [
    'site_key'   => env('TURNSTILE_SITE_KEY'),
    'secret_key'=> env('TURNSTILE_SECRET_KEY'),
    'theme'     => env('TURNSTILE_THEME', 'light'),
    'size'      => env('TURNSTILE_SIZE', 'normal'),
];

この形にした理由はシンプルで、

  • Blade 側からも参照できる
  • バリデーション側からも参照できる
  • カスタムパッケージ内でも config() 経由で扱える

という、役割の分離と見通しの良さを優先しました。

3. Turnstile用のバリデーションルールを作成

Bot 判定の本体は、
独自の Validation Rule として実装します。

<?php

namespace Tool\General\Application\Domain\Rules;

use Closure;
use Illuminate\Contracts\Validation\ValidationRule;
use Illuminate\Support\Facades\Http;

class Turnstile implements ValidationRule
{
    public function validate(string $attribute, mixed $value, Closure $fail): void
    {
        $secretKey = config('turnstile.secret_key');

        if (empty($secretKey)) {
            $fail('Turnstileが正しく設定されていません。');
            return;
        }

        $response = Http::asForm()->post(
            'https://challenges.cloudflare.com/turnstile/v0/siteverify',
            [
                'secret'   => $secretKey,
                'response' => $value,
                'remoteip' => request()->ip(),
            ]
        );

        if (!$response->json('success')) {
            $fail('認証に失敗しました。もう一度お試しください。');
        }
    }
}

メモ

  • FormRequest に直接処理を書かず、Rule として分離
  • DDD構成でも「Domain Rules」に置いて違和感がない
  • Turnstile は
    cf-turnstile-response という 固定名でPOSTされる点に注意

一旦ここまで!続きは次回!

今回は以上です!