menu-icon

Laravel 6で問い合わせフォームを作ってみる3(reCAPTCHA部分)

前回、入力内容をSlackへ投稿する部分を実装しました。最終回となる今回は、Bot対策のため、フォームにreCAPTCHA v3を導入します。

初回の記事はこちら

準備

reCAPCHA作成

reCAPTCHAのサイトで作成します。reCAPTCHAタイプはv3を選択します。

作成出来たらサイトキーとシークレットを控えておきます。

ライブラリインストール

PHP用のクライアントライブラリをcomposerでインストールします。

$ composer require google/recaptcha "^1.2"

実装

環境変数とコンフィグ

.envにサイトキーとシークレットを追加します(サンプルは適当です)。

.env
RECAPTCHA_SITE_KEY=xxxxxxxxxx
RECAPTCHA_SECRET=xxxxxxxxxx

つぎにconfig/services.php に以下を追加し、アプリケーションからconfig経由で読み込めるようにします。

config/services.php
    'recaptcha' => [
        'sitekey'   => env('RECAPTCHA_SITE_KEY'),
        'secret'    => env('RECAPTCHA_SECRET'),
    ],

ビューの変更

入力画面において、フォーム送信時にreCAPTCHAによってトークンを取得し、それをリクエストに含めるようにします。resources/views/inquiry/index.blade.php の scriptセクションを以下のように変更します。

resources/views/inquiry/index.blade.php
@section('script')
<script src="https://www.google.com/recaptcha/api.js?render={{ config('services.recaptcha.sitekey') }}"></script>
<script>
  function onClick(e) {
    // ボタンを無効に
    e.currentTarget.disabled = true;

    grecaptcha.ready(function() {
      grecaptcha.execute("{{ config('services.recaptcha.sitekey') }}", {action: "submit"}).then(function(token) {
        // tokenをフォームにセットして送信
        document.getElementById("recaptcha-token").value = token;
        document.getElementById("contactform").submit();
      });
    });
  }
</script>
@endsection

この変更で、前に定義していた、<input type="hidden" name="token" id="recaptcha-token" />の要素にトークンがセットされて、送信されるようになります。

reCAPCHAの検証

結合(ライブラリのインスタンス生成)

インストールしたreCAPTCHAのライブラリ(ReCaptcha\ReCaptchaクラス)ですが、コンストラクタの第1引数でreCAPTCHAのシークレットを渡す必要がありますので、結合を登録しておきます。今回はapp\Providers\AppServiceProvider.phpで追加しました。

app\Providers\AppServiceProvider.php
<?php

namespace App\Providers;

use Illuminate\Support\ServiceProvider;
use ReCaptcha\ReCaptcha;

class AppServiceProvider extends ServiceProvider
{
    /**
     * Register any application services.
     *
     * @return void
     */
    public function register()
    {
        $this->app->bind(ReCaptcha::class, function ($app) {
            return new ReCaptcha(config('services.recaptcha.secret'));
        });
    }

    /**
     * Bootstrap any application services.
     *
     * @return void
     */
    public function boot()
    {
    }
}

カスタムバリデーションクラス

トークンの検証部分ですが、カスタムバリデーションクラスを使います。まず、Artisanで生成します。

$ php artisan make:rule ReCaptcha

app/Rules/ReCaptcha.phpが生成されたと思います。以下のように編集します。

app/Rules/ReCaptcha.php
<?php

namespace App\Rules;

use Illuminate\Contracts\Validation\Rule;
use ReCaptcha\ReCaptcha as ReCaptchaClient;

class ReCaptcha implements Rule
{
    /**
     * @var ReCaptchaClient $client
     */
    private ReCaptchaClient $client;

    /**
     * Constructor.
     *
     * @param ReCaptchaClient $client
     * @return void
     */
    public function __construct(ReCaptchaClient $client)
    {
        $this->client = $client;
    }

    /**
     * Determine if the validation rule passes.
     *
     * @param  string  $attribute
     * @param  mixed  $value
     * @return bool
     */
    public function passes($attribute, $value)
    {
        $response = $this->client
            ->setScoreThreshold(0.3)
            ->verify($value);

        return $response->isSuccess();
    }

    /**
     * Get the validation error message.
     *
     * @return string
     */
    public function message()
    {
        return 'エラーが発生しました。';
    }
}

passesメソッドでの判定ですが、今回はスコアのみで判定(setScoreThresholdメソッド)しています。実際に使う場合は、ホスト名(setExpectedHostname)やアクション(setExpectedActionメソッド)なども判定に使用すべきかと思います。

フォームリクエストの変更

以前に作成したフォームリクエストApp\Http\Requests\InquiryRequestクラスを編集し、tokenパラメータに対する検証を行うようにします。主な変更箇所はrulesメソッドになります。

app/Http/Requests/InquiryRequest.php
<?php

namespace App\Http\Requests;

use Illuminate\Foundation\Http\FormRequest;
use App\Rules\ReCaptcha;

class InquiryRequest extends FormRequest
{
    /**
     * Get the validation rules that apply to the request.
     *
     * @return array
     */
    public function rules(ReCaptcha $re_captcha)
    {
        return [
            'name'      => ['required', 'max:20'],
            'email'     => ['required', 'email:rfc'],
            'message'   => ['required', 'max:1024'],
            'token'     => ['required', $re_captcha],
        ];
    }

    /**
     * Get custom messages for validator errors.
     *
     * @return array
     */
    public function messages()
    {
        return [
            'required'  => ':attributeは必須です。',
            'max'       => ':attributeは最大:max文字で入力してください。',
            'rfc'       => '正しいメールアドレスを入力してください。',
        ];
    }

    /**
     * Get custom attributes for validator errors.
     *
     * @return array
     */
    public function attributes()
    {
        return [
            'name'      => 'お名前',
            'email'     => 'メールアドレス',
            'message'   => 'メッセージ',
            'token'     => 'ReCaptcha',
        ];
    }
}

実行結果

フォームを表示し、右下にreCAPTCHAのマークが表示されていることを確認します。

普通に入力して送信すると、以前と同じく確認画面へ遷移できることが確認できます。

一方、適正でないと判定されたユーザーの場合、どうなるのでしょうか。reCAPTCHAのスコアを指定はできないので、適正判定のスコアの閾値自体を一時的に変更して確認します。

App\Rules\Recaptchaクラスのpassesメソッド内で指定しているスコアの閾値0.3を1.0に変更します。

app/Rules/Recaptcha.php
    public function passes($attribute, $value)
    {
        $response = $this->client
            ->setScoreThreshold(1.0)
            ->verify($value);

        return $response->isSuccess();
    }

この状態ではフォームに入力して送信しても、下図のようにエラーが表示され、確認画面へ遷移できないはずです。

ということで、reCAPTCHAを導入して、Bot対策を講じることができました。