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される点に注意
一旦ここまで!続きは次回!
今回は以上です!