LaravelにreCAPTCHAを導入してボットによるフォーム操作(スパム行為・攻撃)を根絶する
- 公開:
- 更新:
- カテゴリ: PHP Laravel
- タグ: PHP,Laravel,artisan,5.5,validation,Form,FormRequest,reCAPTCHA
Laravelを使ってWebアプリケーション開発を行っていると、アンケートやお問い合わせ、CMSのログインなど、フォームを使用したページも多く作る機会があると思います。
そんな時に悩まされるのが、ボット(ロボット)の存在です。
一度目をつけられたら最後、サイト内のフォームを使って悪さをしようとしてきます。いたずらレベルから各種攻撃に至るまでその範囲は様々ですが、できるだけ(いや、完全に)排除したいですよね。(やつら自動だからまた嫌味です)
というわけで今回は、Laravelを使って、フォームを作成し、そこにreCAPTCHAを導入して、ボット対策を行います。
reCAPTCHA
reCAPTCHA(リキャプチャ)とは、Googleが提供している認証APIです。その多くはフォームなどに実装されており、「私はロボットではありません」のチェックボックス、及び、画像などをクリックする事で、ボットではない事を証明出来る。いわば、「ボット抑制認証API」です。
これを実装する事で、ボットがサイト内のフォームに自動で送信を行ったりといった事を防ぐ事ができます。
検証環境
今回の検証環境は以下の通りです
- Linux CentOS 7
- Apache 2.4
- PHP 7.1
- Laravel 5.5
インフラやミドルウェアについては、上記と同じでなくても問題ありません。artisanコマンドが叩ければ尚良し、の環境でなら大丈夫です。
また、例のごとく、Laravelのルートディレクトリを「laravel/」とします。
尚、前提条件として、Googleアカウントが1つ必要なので、ブラウザ上からログインしておいてください。
reCAPTCHAを取得する
まずは、reCAPTCHAを使用する為にAPI KEYなどを取得します。公式サイトへアクセスします。
画面右上にある「Get reCAPTCHA」を押下します。
ここで基本情報を入力します。
- Label
- 取得するAPIに名前を付けます。適当に入力します。
- Choose the type of reCAPTCHA
- どの認証パターンにするかを選択します。
-
- reCAPTCHA V2
- 「私はロボットではありません」チェックボックスを使用してユーザーを検証します。今回はここを選択します。
- Invisible reCAPTCHA
- バックグラウンドでユーザーを検証します。
- reCAPTCHA Android
- アンドロイドアプリでユーザーを検証します。
- Domains
- 使用するサイトのドメインを入力します。ローカル環境でも動作するので、ひとまず導入しようと思っているWEBアプリケーションのドメインを入力します。
- Send alerts to owners
- オーナーにアラートを送信するかどうかのチェックボックスです。何かあったら知らせてくれるので、ここもチェックを入れておきます。
入力が完了したら、「Register」ボタンを押下します。
ページ遷移すると、API KEYなどが取得できるのでメモしておきます。(必要なのは「Site key」と「Secret key」そして「URL」です)
Laravel側実装
次に、Laravel側を実装していきます。reCAPTCHAのビュー部分とサーバーサイドの処理以外は基本的な流れなので、各実装部分をダイジェストで紹介していきます。
環境変数の設定
まずはenvファイルへ、reCAPTCHAで取得した情報を記述します。
laravel/.env を開いて、以下を追記します。
RECAPTCHA_API_URL=https://www.google.com/recaptcha/api/siteverify
RECAPTCHA_API_KRY=wE4FVh4XTPFGvpY7inKYJv2-sWa3whhnGSXamXcF
RECAPTCHA_API_SECRET=vGD2GH4VRAZnBmHKi2SEsmUwnWekidCQzqv8TZjU
- RECAPTCHA_API_URL
- あなたが取得した「URL」を設定
- RECAPTCHA_API_KRY
- あなたが取得した「Site key」を設定
- RECAPTCHA_API_SECRET
- あなたが取得した「Secret key」を設定
こういう設定情報系は直接ソースの中に記述せず、設定ファイルで管理する事が望ましいです。
ルーティング
laravel/routes/web.php に以下を追記します。
// フォームページへアクセス
Route::get('sample/cap', 'SampleController@cap');
// フォームページ送信
Route::post('sample/cap', 'SampleController@capPost');
http://YOURDOMAIN/sample/cap
へのアクセスでSampleControllerを呼び、それぞれ、フォームへのアクセス(GET)の場合はcap()メソッドを、フォームから送信した(POST)場合はcapPost()メソッドを呼ぶようにしています。
コントローラ
laravel/app/Http/Controller/SampleController.php を作成し、以下を記述します。
<?php
namespace App\Http\Controllers;
use App\Http\Requests\CapRequest;
class SampleController extends Controller
{
public function cap()
{
return view('sample.cap');
}
public function capPost(CapRequest $request)
{
/* 認証に成功した後の処理を記述する */
return view('sample.cap', ['status' => true]);
}
}
cap()メソッドはGETアクセス、フォームのページへアクセスした場合のアクションです。ここではビューを返すだけのシンプルな記述です。
そしてcapPost()メソッドでは、フォームから送信された場合(POST)のアクションです。引数には後述するCapRequestクラスを取っています。
また、ビューを返す際に、status変数にtrueを入れてセットしていますが、この後に実装するビューで、ここを起点にメッセージを出すので、それ用の値になります。
ビュー
少し前後してしまいますが、先にビューを実装します。
今回は laravel/resources/views/sample/cap.blade.php を作成しました。内容は以下の通りです。
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<title>reCAPTCHAのサンプル</title>
<script src='https://www.google.com/recaptcha/api.js'></script>
<style>
h1 { font-size: 16px; }
.form_wrap { padding: 10px; }
.errors {
width: 300px;
font-size: 12px;
color: #e95353;
border: 1px solid #e95353;
background-color: #f2dede;
}
.complete {
padding-left: 10px;
width: 290px;
font-size: 12px;
color: #2a88bd;
border: 1px solid #2a88bd;
background-color: #a6e1ec;
}
</style>
</head>
<body>
@if ($errors->any())
<div class="errors">
<ul>
@foreach ($errors->all() as $error)
<li>{{ $error }}</li>
@endforeach
</ul>
</div>
@endif
@isset ($status)
<div class="complete">
<p>認証に成功しました</p>
</div>
@endisset
<div class="form_wrap">
<form method="post">
{{ csrf_field() }}
<input type="text" name="sample_01"><br><br>
<div class="g-recaptcha" data-sitekey="{{env('RECAPTCHA_API_KRY')}}" data-callback="recaptchaCallback"></div><br><br>
<button type="submit" id="submit" disabled>送信</button>
</form>
</div>
<script> function recaptchaCallback(param) { if(param) { document.getElementById('submit').disabled = ""; } } </script>
</body>
</html>
head内に記述しているCSSは主にメッセージ系のスタイルです。そして、blade記法である「@if」「@isset」で書かれているのが、メッセージ部分です。バリデーションや認証に通らなければ赤字でエラーメッセージが、通れば青字で成功しましたと表示するようになっています。
以下、reCAPTCHA実装部分なので細かく説明していきます。
<script src='https://www.google.com/recaptcha/api.js'></script>
head内でreCAPTCHAのライブラリを読み込んでいます。head内なのは、そうしろと公式に記載があったからですが、下に持って行っても一応は動くみたいです。
<div class="g-recaptcha" data-sitekey="{{env('RECAPTCHA_API_KRY')}}" data-callback="recaptchaCallback"></div>
この部分が、reCAPTCHAの認証を表示する個所です。
「data-sitekey」では、envファイルからAPI KEYを取得しセットしています。
「data-callback」では、クリックされ認証に通った場合に、コールバックを呼び出す事が出来るのですが、そのコールバック関数をここで指定しています。
<script> function recaptchaCallback(param) { if(param) { document.getElementById('submit').disabled = ""; } } </script>
このJavaScriptはreCAPTCHAの認証が通った際に呼ばれるコールバック関数です。
HTMLの送信ボタンを見るとわかる通り、初期状態では「disabled」としボタンを押下できなくしていますが、このコールバックが呼ばれ、実際に認証されている事が確認できた場合には、disabledを外して押下出来る状態にしています。
フォームリクエストクラス
最後に、フォームリクエストクラスを実装します。
コントローラが極めてシンプルな造りだったと思いますが、フォームのバリデーションやreCAPTCHAのサーバサイドの認証はここで実装していきます。
メインはreCAPTCHAなので、フォームバリデーションやreCAPTCHAの認証はコントローラに記述しても良いかとは思ったのですが、せっかくLaravelを使って実装するのであれば、良さを生かして書くのがベストかと思いますので、このロジックを守る事にしました。
尚、フォームリクエストクラスについてよくわからない場合は以下を参考にしてください。
Laravel5.5のフォームリクエストクラスでバリデーションロジックをコントローラから分離する
ついでに、バリデーションの基本はこちらをどうぞ
Laravel5.5のvalidationメソッドでバリデーションを実装する
それでは、フォームリクエストクラスlaravel/app/Http/Requests/CapRequest.phpを生成し、以下のように実装します。
<?php
namespace App\Http\Requests;
use Illuminate\Foundation\Http\FormRequest;
class CapRequest extends FormRequest
{
public function authorize()
{
return true;
}
public function rules()
{
return [
'sample_01' => 'required|string',
];
}
public function messages()
{
return [
'name.required' => '何か入力してください',
'name.string' => '文字列で入力してください',
];
}
public function withValidator($validator)
{
$validator->after(function ($validator) {
$response = json_decode(file_get_contents(
sprintf("%s?secret=%s&response=%s", env('RECAPTCHA_API_URL', ''), env('RECAPTCHA_API_SECRET', ''), $this->input('g-recaptcha-response'))
),true);
if(!$response['success']) {
$validator->errors()->add('field', '認証に失敗しました');
}
});
}
}
rules()メソッドとmessages()メソッドに関しては、通常のフォームバリデーションなので解説は割愛します。reCAPTCHAのサーバ側の認証処理はwithValidator()メソッドに記述しています。
$response = json_decode(file_get_contents(
sprintf("%s?secret=%s&response=%s", env('RECAPTCHA_API_URL', ''), env('RECAPTCHA_API_SECRET', ''), $this->input('g-recaptcha-response'))
),true);
reCAPTCHAのAPIへ、POSTで受け取った認証コードを投げて正常に認証出来ているものかどうかを受け取っています。
実は送信ボタンが押下された時に、常設のフォームの値と一緒に、ユーザが認証したreCAPTCHAの認証コードが送られていきています。以下の部分です。
$this->input('g-recaptcha-response')
これをAPI SECRETと共にreCAPTCHAのAPIへ投げており、結果がJSONで返ってくるので正しく認証されたものかどうかを判別できるという事になります。
わかりやすく分解して書くと以下のようになります。
// リクエストURLの組み立て「https://www.google.com/recaptcha/api/siteverify?secret=[API SECRET]&response=[ユーザ認証コード]」
$request_url = sprintf("%s?secret=%s&response=%s", env('RECAPTCHA_API_URL', ''), env('RECAPTCHA_API_SECRET', ''), $this->input('g-recaptcha-response'));
// reCAPTCHA API へリクエストを送り結果を受け取る
$response = file_get_contents($request_url);
// JSON形式を配列へ変換する
$response_array = json_decode($response, true);
// $response_array
Array
(
[success] => 1
[challenge_ts] => 2017-12-19T08:37:48Z
[hostname] => laravel55-practice.com
)
結果配列の[success]の中に、認証結果が入ってくるので、そこで判断し、エラーならエラーメッセージを追加している。という流れになります。
動作確認
それでは、全ての実装が完了したので動作確認を行います。ブラウザからhttp://YOURDOMAIN/sample/capへアクセスしてみます。
reCAPTCHAの画面が表示されています。(一番上にある四角枠は、テキスト入力フォームです。)
よく見ると、送信ボタンがOFFになっているのも確認できるでしょうか?
「私はロボットではありません」にチェックを入れたら、画像クリック認証が表示されました。
ちなみに、画像認証は出現しない場合もあります(=チェックするだけで完了する)。この辺は、reCAPTCHA側の規則に基づいて表示されています。
画像認証も無事に通りました。コールバック関数で送信ボタンも押下出来る状態になりました。
送信ボタンを押下し、サーバサイドの認証も通ったのでreCAPTCHA APIの実装は成功です。
まとめ
以上で作業は完了となります。
もし、Javascriptでの送信ボタン制御を行わない場合は、認証にチェックを入れなくても送信ができてしまいますが、サーバサイドの認証でエラーとなります。
簡単に導入出来て、少しでもサイトを攻撃やいたずらから守れる良いAPIだと思いますので、ぜひ試してみてください。
今回の作業ソース一式はGithubから取得できます。
[Github]rito-nishino/www.ritolab.com-sample-sources-Laravel5.5-reCAPTCHA