Laravel + Vue + axios でreCAPTCHAバージョン3を実装する

こんにちは。フリーランス・コンサルタント&エンジニアの 九保すこひ です。

さてさて、前回の記事ではAjaxに関連する記事をお届けしました。Ajaxは現在ウェブ開発では本流といっていいほどよく利用されているテクニックですが、HTTP送信と同じくデータ送信であることには違いがありません。

そして、データ送信する際に必ず考えておかなければいけないことがあります。それは、

自動ロボットによる無差別なスパム送信

です。

どういうことかというと、スパムを実行する人たちの戦略のひとつは次のようなものになります。

  1. どこからでもいいので無差別にデータ送信する
  2. もしその送信がお問い合わせならメッセージ送信できる
  3. メッセージに自分のサイトや広告リンクを入れておく

こうすることで、数撃ちゃ当たる方式でアクセスを増やそうをしているわけですね。

ただ、正直ウェブサイト開発する我々からすると、こういった無差別送信は「百害あって一利なし」なので、すべて遮断することが望ましいです。

そこでよく利用されているのがGoogleが提供する reCAPTCHA です。
例の「私はロボットではありません」と表示されるやつですね。

ただし、この記事を書いている時点ではreCAPTCHAはバージョン3になり、なんと何もしなくてもロボットか人間かの判別をしてくれるようになっています。

そこで!
今回はLaravel + Vue + axios のAjax送信でreCAPTCHAを使う方法をご紹介したいと思います。

「スパム送信やめてほしい・・・😭」

開発環境: Laravel 5.8, Vue 2.6, axios 0.18

reCAPTCHAにサイトを登録する

まずはGoogleアカウントにログインして以下のURLにアクセスします。

https://www.google.com/recaptcha/admin

すると画面右上にボタンがありますのでここをクリックしてサイト登録のページへ移動します。

移動したら必要な項目を入力します。

  • ラベル ・・・ サイトを識別するテキスト。お好みで決めていいですが後から見てわかりやすいものを指定することをおすすめします。(例:サイトドメインやサイト名など)
  • reCAPTCHAタイプ ・・・ 今回はバージョン3の記事ですのでreCAPTCHA v3を選択
  • ドメイン ・・・ reCAPTCHAを有効にするドメイン。複数指定できますが、ローカル環境とサーバー環境は分けるべきとされています。
  • 利用条件 ・・・ 同意する

入力が終わったらページ下部にある送信ボタンをクリックしてください。

すると、以下のようにreCAPTCHAを利用するために必要な2つのキーコードが表示されます。

この2つのキーコードをLaravelからいつでもアクセスできるように.envファイル内に以下のように書き込んでおきましょう。

RECAPTCHA_SITE_KEY=*********************
RECAPTCHA_SECRET_KEY=*************************

はい!
これでreCAPTCHAの設定は完了です。

(ちなみに)

キーコードを後から確認したい場合はページ右上にあるギアのマークをクリックします。

すると、「reCAPTCHAのキー」という項目があるのでそこをクリックすると以下のように表示されます。

HTML部分をつくる

では続いて、HTML部分を作っていきます。
まずは実際のコードから。

<html>
<body>
    <div id="app">
        <!-- 1 -->
        <button type="button" @click="onSubmit">送信</button>
    </div>
    <!-- 2 -->
    <script src="https://www.google.com/recaptcha/api.js?render={{ env('RECAPTCHA_SITE_KEY') }}"></script>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/axios/0.18.0/axios.min.js"></script>
    <script src="https://cdn.jsdelivr.net/npm/vue@2.6.10/dist/vue.min.js"></script>
    <script>

        new Vue({
            el: '#app',
            data: {
                // 3
                params: {
                    token: ''
                }
            },
            methods: {
                onSubmit() {

                    axios.post('/recaptcha_ajax', this.params)
                        .then((response) => {

                            console.log(response.data)

                        })

                }
            },
            mounted() {

                // 4
                grecaptcha.ready(() => {
                    grecaptcha.execute('{{ env('RECAPTCHA_SITE_KEY') }}', {action: 'homepage'})
                        .then((token) => {

                            this.params.token = token

                        });
                });

            }
        })

    </script>
</body>
</html>

やっていることは次のとおりです。

1 ・・・ クリックするとAjax送信するonSubmit()というイベントをつけます。送信内容にはreCAPTCHAから取得したトークン(パスワードのようなもの)が含まれることになります。

2 ・・・ 各種JavaScriptライブラリをcdnで読み込んでいます。上から、reCAPTCHA、axios、Vueです。reCAPTCHAのURLには.envに書き込んだサイト・トークンが含まれていることに注意してください。

3 ・・・ Vueの変数としてparamsを定義します。この変数内のtokenreCAPTCHAのトークンが格納されます。

4 ・・・ ページの読み込みが完了した時点でreCAPTCHAからトークンを取得します。grecaptcha.execute()にもサイト・トークンが含まれています。

※ 今回はテストですので送信データをトークンのみにしていますが、実際の開発ではparams内にメールアドレスやパスワードなどのデータを格納して送信するといいでしょう。

なお、このコードをブラウザで実行するとページに右下にreCAPTCHAのマークが表示されます。

そして、マウスカーソルを合わせると次のようになります。

Laravel部分をつくる

では最後にLaravel部分を作っていきましょう。

準備

取得したトークンをPOST送信するために便利なのでGuzzleという有名なHTTPクライアント・パッケージをインストールしましょう。

composer require guzzlehttp/guzzle

バリデーション・ルール

続いてAjax送信されたときに実行するバリデーション・ルールを定義します。まずは以下のコマンドで独自ルールのファイルを作成しましょう。

php artisan make:rule Recaptcha

これを実行するとapp/Rules/Recaptcha.phpというファイルが作成されるので、開いて以下のように変更します。

<?php

namespace App\Rules;

use GuzzleHttp\Client;
use Illuminate\Contracts\Validation\Rule;

class Recaptcha implements Rule
{
    private $_base_score;

    /**
     * Create a new rule instance.
     *
     * @return void
     */
    public function __construct($base_score = 0.5)
    {
        // 1
        $this->_base_score = $base_score;
    }

    /**
     * Determine if the validation rule passes.
     *
     * @param  string  $attribute
     * @param  mixed  $value
     * @return bool
     */
    public function passes($attribute, $value)
    {
        // 2
        $client = new Client();
        $response = $client->request('POST', 'https://www.google.com/recaptcha/api/siteverify', [
            'form_params' => [
                'secret' => env('RECAPTCHA_SECRET_KEY'),
                'response' => $value
            ]
        ]);
        $results = json_decode($response->getBody(), true);
        return (
            $results['success'] &&
            $results['score'] > $this->_base_score
        );
    }

    /**
     * Get the validation error message.
     *
     * @return string
     */
    public function message()
    {
        // 3
        return '機械的なアクセスだと判定されました。リロードしてもう一度実行してください。';
    }
}

重要なのは次の3点です。

1 ・・・ コンストラクタで「基準となるスコア」を指定できるようにします。スコアとは、reCAPTCHAが「どれくらい機械じゃないと判別したか」がわかる数字で、0.0〜1.0の数字になります。スコアの良し悪しは次のとおりです。

  • 0.0 ・・・ ほぼ確実にロボット
  • 0.1 ~ 0.4 ・・・ 省略
  • 0.5 ・・・ デフォルト値。これより大きければ人間、小さければロボットと判別されたことになります。
  • 0.6 ~ 0.9 ・・・ 省略
  • 1.0 ・・・ ほぼ確実に人間

2 ・・・ 先ほどインストールしたGuzzleを使ってトークンとシークレットキーを同時にreCAPTCHA側へ送信し、判別データを取得しバリデーションが成功したかどうかをtrue, falseで返します。

3 ・・・ バリデーションに失敗した場合のエラーメッセージです。多言語化しているならlang内に書くべきですが、今回はテストなので直に書き込んでいます。

フォームリクエスト

では、reCAPTCHAの独自バリデーションを実行する部分を作っていきましょう。まずは以下のArtisanコマンドでフォームリクエストを作ります。

php artisan make:request TestRequest

作成が完了したらapp/Http/Requests/TestRequest.phpを開いて以下のように変更します。

<?php

namespace App\Http\Requests;

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

class TestRequest extends FormRequest
{
    /**
     * Determine if the user is authorized to make this request.
     *
     * @return bool
     */
    public function authorize()
    {
        return true;
    }

    /**
     * Get the validation rules that apply to the request.
     *
     * @return array
     */
    public function rules()
    {
        $base_score = 0.5;
        return [
            'token' => new Recaptcha($base_score)
        ];
    }
}

太字の部分が変更した部分です。

重要な部分としては、new Recaptcha()には引数で基準となるスコアを指定しているところです。つまり、ここでお好みのスコア基準を指定できるわけですね。(ちなみに省略すると基準スコアは0.5になります)

では、フォームリクエストの作成も完了しましたのでコントローラーに設置しましょう。

public function recaptcha_ajax(TestRequest $request) {

    return [
        'result' => true
    ];

}

これで、バリデーションが成功したら['result' => true]axiosに返されるようになります。

お疲れ様でした!

問題点

今回reCAPTCHA v3を利用するにあたって2点ほど問題点に遭遇したので最後にその解決策をご紹介します。

問題点1:アクセスしてすぐ送信できない

つまり、まだreChaptcha側からトークンを取得する前なのにデータ送信されてしまうと、もちろんトークンは空ですからバリデーションは失敗してしまいます。

これを回避するには、以下のようにv-ifでトークンが取得されるまでは送信ボタンを非表示にしておくか、もしくはdisabledを切り替えるようにするといいでしょう。

<button type="button" v-if="params.token" @click="onSubmit">送信</button>

問題点2:トークンは一回しか使えない

残念ながらトークンが有効なのは1回だけです。
そのため、一度Ajax送信したら再度新しいトークンを取得しないといけません。

これを実行するためコードを以下のように変更しました。

(HTML部分)

<button type="button" v-if="params.token && !isSubmitting" @click="onSubmit">送信</button>

(JavaScript部分)

<script>

    new Vue({
        el: '#app',
        data: {
            params: {
                token: ''
            },
            isSubmitting: false
        },
        methods: {
            onSubmit() {

                this.isSubmitting = true;

                axios.post('/recaptcha_ajax', this.params)
                    .then((response) => {

                        // 送信成功

                    })
                    .catch((error) => {

                        // ここでエラー処理

                    })
                    .then(() => {

                        // Ajax送信後必ず実行する
                        this.isSubmitting = false;
                        this.setRecaptchaToken();

                    })

            },
            setRecaptchaToken() {

                grecaptcha.ready(() => {
                    grecaptcha.execute('{{ env('RECAPTCHA_SITE_KEY') }}', {action: 'homepage'})
                        .then((token) => {

                            this.params.token = token

                        });
                });

            }
        },
        mounted() {

            this.setRecaptchaToken();

        }
    })

</script>

やっているのは、まずsetRecaptchaToken()というメソッドをどこからでもトークン取得を実行できるようにします。

そして、このsetRecaptchaToken()はページ表示された時、そしてAjax送信が終わった時の2ヵ所で呼び出します。

これでトークンはAjaxが送信されるたびに新しいものに置き換わることになりますが、まだそれでも問題点が残されています。

それは、連続クリックです。

Ajax送信が完了する前に何度もクリックされてしまうと、一回しか使えないトークンを2回以上使うことになってしまうため、エラーが発生してしまうことになります。

そのため、isSubmittingという変数で送信中かどうかを判別しボタンの表示/非表示を切り替えています。

ダウンロード

今回実際に開発したコードを以下からダウンロードすることができます。

※ ただし、.envの設定やパッケージのインストールはこのページを呼んでご自身で設定してください。

Laravel + Vue + axios でreCAPTCHAバージョン3を実装
開発のご依頼お待ちしております
開発のご依頼はこちらから: お問い合わせ
どうぞよろしくお願いいたします! by 九保すこひ

おわりに

これまでreCAPTCHAにはずっとお世話になってきましたが、今回ははじめてバージョン3を試すことになりました。

正直なところバージョン2は、クリック一発ですんなりOKの場合もありますが、なんせローカル環境でテストしてると何度も何度もデータ送信しないといけないわけで、そうなってくると例の画像が分割されていて「車を選べ」とか「標識を選べ」みたいな選択画面が何度も何度も表示されることになり、イライラさせられたことが何度もありました。

そう考えると今回のバージョン3はユーザー側で何もする必要がないのでとてもユーザビリティが向上しているんじゃないでしょうか。

(・・・でも、どんな仕組みで人間とロボットを判別してるんでしょうか・・・やっぱ機械学習??)

ということで今回は最新のreCAPTCHAの話題をお届けしました。
皆さんのお役にたてると嬉しいです。

ではでは!

このエントリーをはてなブックマークに追加       follow us in feedly