reCAPTCHAのフロントエンド実装とバックエンド実装(PHP・Laravel)をスクラッチで行う方法
技術スタック PHPJavascriptLaravelセキュリティ

reCAPTCHAのフロントエンド実装とバックエンド実装(PHP・Laravel)をスクラッチで行う方法

2021.05.21

こんにちはjunです。皆さんはフォームを作成する時にBot・スパム対策を行っていますか?フォームというのは少し知識があれば、簡単にbot的に送信することができます。

curlでPOSTすることもあれば、javascriptを実行して機械的にフォームを送信することがきるので、スパム(嫌がらせ)やサーバーへの過剰負荷の原因となります。

この様な機械的な操作を防ぐために、よく「ロボットではありません」「画像に表示されている文字を入力してください」みたいなbotでは簡単に処理できないものを用意します。

しかしこの様な機能は自分で実装するのは大変です。そんな時に便利なのがreCAPTCHAです。

reCAPTCHAとは?

reCAPTCHAはGoogleが無料で提供しているBot対策ツールです。現在v3までリリースされており、v2は画像を選択させたりチェックを入れるといったユーザーのアクションでbotかどうかを検証します。v3はその様なアクションを必要とせず、必要なスクリプトを入れるだけで検証ができます。これからの実装の場合はv3を入れることをお勧めします。

実装内容

今回の解説ではv3でのフロントエンドの実装とバックエンドの実装を解説していきます。そして利用するreCAPTCHAはエンタープライズではなく、無料のものを利用します。バックエンドにはLaravel6(php)を用いて説明します。バックエンドは基本的にやることはどの言語・フレームワークでも特に差異はありません。reCAPTCHAを使う時にライブラリを使用することもありますが、いうてそれほど難しくないので今回はライブラリを使用しないスクラッチで実装します。

それでは解説を始めます。

reCAPTCHAのキーを手に入れる

reCAPTCHAはGooglenのAPIを使用することでbotか検証を行います。reCAPTCHAを利用するためにGoogleアカウントとreCAPTCHAのキーを登録します。reCAPTCHAの登録ページにて保護対象のドメインを設定します。

ラベルは管理用の名前、タイプはv3を使用します。保護対象のドメインを登録します。ドメインは複数設定できます。この時、本番のドメインとlocalhostを登録しておくと開発・本番で利用できます。

設定を終わって「保存」しますと、キーが表示されますので保存しておきます。

このサイトキーは、ユーザー表示するHTMLコードで利用します。 という方のキーはタグで利用し、正直みられても問題ありません。フロント側のキーはドメインと合わせて保護対処のサイトであるかのチェックのためにあるだけだからです。逆にバックの方である このサイトキーは、サイトとreCAPTCHA間の通信で利用します。 は漏れてはいけません。

フロントエンドの実装

それではフロントエンドを実装していきます。かなり簡略化して書いています。以下の様なフォームがあったとしましょう。

<!DOCTYPE html>
<html>
    <head>
      <!---省略-->
    </head>

    <body>
        <form method="post">
            <input type="text" name="test" value="">
            <input type="submit" value="送信">
        </form>
    </body>
</html>

reCAPTCHAのフロントエンド 実装では

  • reCAPTCHAのソースを読み込む
  • 送信ボタンを押したらreCAPTCHAと通信してトークンを手に入れるスクリプトを書く
  • reCAPTCHAのトークンをフォーム内容と一緒にバックエンドに送信する。

以上の実装を必要とします。本家のドキュメントを参考にして進めていきましょう。

reCAPTCHAのスクリプト読み込みとHTML調整

まずはreCAPTCHAを有効にするためのスクリプトを読み込みます。そして一部フォームを編集します。

<!DOCTYPE html>
<html>
    <head>
      <!---省略-->
        <script src="https://www.google.com/recaptcha/api.js?render=YOUR_FRONT_KEY"></script><!---追加-->
    </head>

    <body>
        <form method="post" id="test-form"><!---追加-->
            <input type="text" name="test" value="">
            <input type="hidden" name="recaptcha" value=""><!---追加-->
            <input type="submit" value="送信">
        </form>
    </body>
</html>

<script src="https://www.google.com/recaptcha/api.js?render=YOUR_FRONT_KEY"></script>YOUR_FRONT_KEYに先ほど取得したフロント用のキーを入れます。 <input type="hidden" name="recaptcha" value="">にはreCAPTCHAのトークンを挿入してバックエンドに送ります。HTMLフォームであればこの様にしますが、axiosなどの場合はトークンの値をjsを用いて送信するので、このHTMLは要りません。

トークンを取得するスクリプトを記述

「送信ボタン」を押した時にreCAPTCHAにAPIを飛ばしてbotかどうかの判定用トークンを取得します。以下の様なスクリプトを記述します。

<!DOCTYPE html>
<html>
    <head>
      <!---省略-->
      <script src="https://www.google.com/recaptcha/api.js?render=YOUR_FRONT_KEY"></script>
    </head>

    <body>
        <form method="post" id="test-form"><!---追加-->
            <input type="text" name="test" value="">
            <input type="hidden" name="recaptcha" value=""><!---追加-->
            <input type="submit" value="送信">
        </form>
    </body>

    <script>
        function checkCaptcha(e) {
            e.preventDefault();
            grecaptcha.ready(function() {
                grecaptcha.execute('YOUR_FRONT_KEY', {action: 'submit'}).then(function(token) {
                    document.getElementById("recaptcha").value=token;
                    document.getElementById("test-form").submit();
                });
            });
        }
        document.getElementById("test-form").addEventListener('submit', checkCaptcha);
        </script>
</html>

フォームの送信ボタンが押された時(submitイベント発火時)にcheckCaptchaの関数が実行される様に設定します。e.preventDefault();を使用してそのままフォームが送信されない様にします。

reCAPTCHAのスクリプトによってgrecaptchaというオブジェクトが使用できる様になり、その中のgrecaptcha.execute()にてAPIを実行します。第一引数にフロントのキー、第二引数にアクションを入力します。Promiseなのでthen(token)内のコールバックでトークンを<input type="hidden" name="recaptcha" value="">に突っ込みます。そしてフォームをsubmit()にて送信します。

これでフロントの実装は完了です。フロントでの動きをみてreCAPTCHAはbotかどうかを判断し、この送信を一意なトークンで保存しているのです。トークンはバックエンドでの検証で利用します。

バックエンドの実装

それではバックエンドの実装をすすめます。Laravelのコントローラーでの記述を想定しています。バリデーションなどは各自設定してください。

バックエンドで行うことは

  • フロントからきたトークンをreCAPTCHAのAPIに送信
  • reCAPTCHAの結果を取得する
  • 結果(スコア)を用いてbotかの判断をする

以上となります。コードは以下の通りです。

class Controller extends BaseController
{
    use AuthorizesRequests, DispatchesJobs, ValidatesRequests;

    public function checkRecaptcha(Request $request){
        try {
            $client = new \GuzzleHttp\Client([
                'headers' => [
                    'Content-Type' => 'application/json',
                ],
            ]);
    
            $promise = $client->postAsync('https://www.google.com/recaptcha/api/siteverify',
            [
                'form_params' =>[
                    'secret'=>env('RECAPTCHA_SERVER_KEY'),
                    'response'=>$request->recaptcha
                ]
            ]);
    
            $res = Promise\Utils::settle($promise)->wait();
            $isFulfilled = isset($res[0]['value']);
            if(!$isFulfilled) throw new \Exception('RECAPTCHA SERVER returns error');
    
            $result = json_decode($res[0]['value']->getBody()->getContents(),true);
            
            if(isset($result['error-codes'])){
                if($result['error-codes'][0] === 'timeout-or-duplicate') return false;
                throw new \Exception('RECAPTCHA SERVER returns error:'.$result['error-codes'][0]);
            }

            return $result['score'] > 0.5 && $result['success'];
        }catch (\Exception $e) {
            report($e);
            return false;
        }
    }
}

recaptchaとのAPI通信にはGuzzleを使用していますが、とにかくAPI通信ができれば大丈夫です。APIはhttps://www.google.com/recaptcha/api/siteverifyにPOSTを送信します。POSTには以下の値が必要です、

'form_params' =>[
    'secret'=>env('RECAPTCHA_SERVER_KEY'),
    'response'=>$request->recaptcha
]

env('RECAPTCHA_SERVER_KEY')はバックエンドで使用するrecaptchaキーです。$request->recaptcha<input type="hidden" name="recaptcha" value="">で挿入されたフロントで取得したrecaptchaのトークンです。このトークンとキーを合わせて、 保護対象のサーバーであり、検証を行うフォーム送信 を判別しています。

通信が成功すると以下の様なレスポンスが戻ります。

[
  "success"=> true, 
  "score"=> 0.8,
  "action"=> string,
  "challenge_ts"=> timestamp,
  "hostname"=> string,
]

一番重要なのは"score"=> 0.8です。このスコアは入力したリクエストがbotか人間かのスコアを示しており、1に近いほど人間が入力しています。逆に0.1あたりはbotの入力です。どこまで厳しくするかはお任せしますが、私は0.5以上であれば人間のリクエストであるとしています。return $result['score'] > 0.5 としてfalseであればリクエストを拒否したり、エラーを返す様にします。フォーム系で汎用的に使用できる様に私はサービスプロバイダにしています。

エラー処理

エラーの場合は以下の様なレスポンスがきます。(例です)

[
  "success"=> false, 
  "action"=> string,
  "challenge_ts"=> timestamp,
  "hostname"=> string,
  "error-codes": [
      0=>'timeout-or-duplicate'
  ] 
]

このエラーはAPIの通信が失敗したり、必要なパラメーターが不足していたりなどのエラーです。 リクエストがBotである という意味ではないので注意。Botかの判定はあくまで成功時に取得するscoreで判定します。

エラーの説明

missing-input-secret env('RECAPTCHA_SERVER_KEY')のようなサーバー側のrecaptchaのキーを忘れている。

'form_params' =>[
    'secret'=>env('RECAPTCHA_SERVER_KEY'), // このへん
    'response'=>$request->recaptcha
]

invalid-input-secret env('RECAPTCHA_SERVER_KEY')が不正。間違っているキーを使用している。キーが正しいか、保護対象のドメインとして登録しているかを確認。

missing-input-response 'response'=>$request->recaptchaを忘れている、空文字。

invalid-input-response 'response'=>$request->recaptchaの値が不正。型などを確認。

bad-request POSTで送っているかを確認。

timeout-or-duplicate 'response'=>$request->recaptchaの値を二回送っているか、フロントのトークンが2分以上経過した。

フロントで取得したトークンはバックエンドでのこの検証を行うともう一度利用することができません。またこのトークンは

grecaptcha.execute('YOUR_FRONT_KEY', {action: 'submit'}).then(function(token) {
    document.getElementById("recaptcha").value=token;
    document.getElementById("test-form").submit();
});

の実行から2分以内で利用する必要があります。そのためページがリロードされた瞬間ときに実行していると、フォーム入力中に時間切れになったります。そのためsubmit時に実行することをお勧めします。

実装まとめ

以上がrecaptchaの実装方法です。recaptchaはあくまでBotかどうかの判断のみをしているので、実際にリクエストを通すかはアプリケーション側の仕事です。フロントとバックでの実装が少し面倒ですが、recaptchaの機能を自前で実装しようとするとそこそこ、面倒なのと実績のあるGoogle様に検証してもらうのも結構安心です。

バックエンドはLaravelを想定しますが、他のフレームワークや言語でもやることは特に変わりません。上手くご自身の環境に置き換えてください。

Copyright © 2021 jun. All rights reserved.