九保すこひ@フリーランスエンジニア|累計300万PVのブログ運営中
さてさて、前回の記事ではAjaxに関連する記事をお届けしました。Ajaxは現在ウェブ開発では本流といっていいほどよく利用されているテクニックですが、HTTP送信と同じくデータ送信であることには違いがありません。
そして、データ送信する際に必ず考えておかなければいけないことがあります。それは、
自動ロボットによる無差別なスパム送信
です。
どういうことかというと、スパムを実行する人たちの戦略のひとつは次のようなものになります。
- どこからでもいいので無差別にデータ送信する
- もしその送信がお問い合わせならメッセージ送信できる
- メッセージに自分のサイトや広告リンクを入れておく
こうすることで、数撃ちゃ当たる方式でアクセスを増やそうをしているわけですね。
ただ、正直ウェブサイト開発する我々からすると、こういった無差別送信は「百害あって一利なし」なので、すべて遮断することが望ましいです。
そこでよく利用されているのが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
を定義します。この変数内のtoken
にreCAPTCHA
のトークンが格納されます。
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
の設定やパッケージのインストールはこのページを呼んでご自身で設定してください。
おわりに
これまでreCAPTCHAにはずっとお世話になってきましたが、今回ははじめてバージョン3を試すことになりました。
正直なところバージョン2は、クリック一発ですんなりOKの場合もありますが、なんせローカル環境でテストしてると何度も何度もデータ送信しないといけないわけで、そうなってくると例の画像が分割されていて「車を選べ」とか「標識を選べ」みたいな選択画面が何度も何度も表示されることになり、イライラさせられたことが何度もありました。
そう考えると今回のバージョン3はユーザー側で何もする必要がないのでとてもユーザビリティが向上しているんじゃないでしょうか。
(・・・でも、どんな仕組みで人間とロボットを判別してるんでしょうか・・・やっぱ機械学習??)
ということで今回は最新のreCAPTCHA
の話題をお届けしました。
皆さんのお役にたてると嬉しいです。
ではでは!