1. Home
  2. PHP
  3. Laravel
  4. LaravelにreCAPTCHAを導入してボットによるフォーム操作(スパム行為・攻撃)を根絶する

LaravelにreCAPTCHAを導入してボットによるフォーム操作(スパム行為・攻撃)を根絶する

  • 公開日
  • 更新日
  • カテゴリ:Laravel
  • タグ:PHP,Laravel,artisan,validation,Form,FormRequest,reCAPTCHA
LaravelにreCAPTCHAを導入してボットによるフォーム操作(スパム行為・攻撃)を根絶する

Laravel を使って Web アプリケーション開発を行っていると、アンケートやお問い合わせ、CMS のログインなど、フォームを使用したページも多く作る機会があると思います。

そんな時に悩まされるのが、ボット(ロボット)の存在です。

一度目をつけられたら最後、サイト内のフォームを使って悪さをしようとしてきます。いたずらレベルから各種攻撃に至るまでその範囲は様々ですが、できるだけ(いや、完全に)排除したいですよね。(やつら自動だからまた嫌味です)

というわけで今回は、Laravel を使って、フォームを作成し、そこに reCAPTCHA を導入して、ボット対策を行います。

Contents

  1. reCAPTCHA
  2. 検証環境
  3. reCAPTCHA を取得する
  4. Laravel 側実装
    1. 環境変数の設定
    2. ルーティング
    3. コントローラ
    4. ビュー
    5. フォームリクエストクラス
  5. 動作確認

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
    • どの認証パターンにするかを選択します。
      • qreCAPTCHA 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=YOUR-API-KRY
RECAPTCHA_API_SECRET=YOUR-API-SECRET
  • 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

Author

rito

  • Backend Engineer
  • Tokyo, Japan
  • PHP 5 技術者認定上級試験 認定者
  • 統計検定 3 級