PHP
Laravel
セキュリティ
reCAPTCHA
認証

Google reCAPTCHAの実装方法【v2非表示 ver】【Laravel】


はじめに

Bot攻撃による大量申し込みを受けてreCAPTCHAを導入することになったのでその手順を記します。

reCAPTCHAの実装方法がよく分からない..という方は参考になるかもしれません。

v2とv3がありますが、なるべくユーザーによる申し込みを弾かないように、かつ最低限のBot対策はできるようにしたかったので、

v2の非表示バージョンで実装をしました。

v2のチェックボックスやv3の方法は今回扱っていません。

分かりやすくするためにPHPフレームワークのLaravelを例にとって説明していますが、他のフレームワークやいわゆるVanilla PHPでも同じように実装できるかと思います。

環境はLAMPを想定しています。


reCAPTCHA Invisibleの設定手順


管理画面での設定

まずはサイトごとのAPIキーを発行しないといけないので管理画面で設定していきます。

Google reCAPTCHAのサイトへアクセス。

https://www.google.com/recaptcha/intro/v3.html

Image201905211254.png

Admin consoleへ移動したら、新しくサイトを追加します、

Image201905211256.png

「ラベル」は好きな名前で大丈夫です。

「セキュリティ設定」はレベルが3段階あり、

2~3のレベルで設定してみたところ、ユーザーによる申し込みも画像認証で弾かれてしまうことがあったので、

「ユーザーの負担が最小」である1段階のレベルで設定をすることに決めました。

grc2.png

上記で確定すると、APIのサイトキー・シークレットキーが発行されます。

次に、Laravel上で設定作業を行っていきます。


JSタグの設置

まずAPIのJSファイルを読み込みます。

テンプレートファイルの<head>内に挿入。


resources/views/form/input.blade.php

<!doctype html>

<html lang="{{ str_replace('_', '-', app()->getLocale()) }}">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>FooBar Form</title>
+ <script src="https://www.google.com/recaptcha/api.js" async defer></script>
</head>
<body>
// contents

<form action="entry/input/" method="post" id="form">
// form inputs
</form>
</body>
</html>



フロント側のタグ・スクリプト設定


configを設定

APIキーはバージョン管理しないほうがよいので.envで設定します。

APP_RECAPTCHA_SITE_KEYは先ほど管理画面で発行したサイトのキー、

APP_RECAPTCHA_SECRET_KEYは先ほど発行したシークレットキーです。


.env

+ APP_RECAPTCHA_SITE_KEY=your_site_key

+ APP_RECAPTCHA_SECRET_KEY=your_secret_key

設定したファイルを読み込みます。

APIのエンドポイントもここに設定しておきます。


config/app.php

    /*

|--------------------------------------------------------------------------
| reCAPTCHA Settings
|--------------------------------------------------------------------------
|*/
+ 'recaptcha' => [
+ 'api_url' => 'https://www.google.com/recaptcha/api/siteverify',
+ 'site_key' => env('APP_RECAPTCHA_SITE_KEY'),
+ 'secret_key' => env('APP_RECAPTCHA_SECRET_KEY'),
+ ],

];


レンダリング対象となるdivタグ設置

reCAPTCHAのレンダリングに必要となるので設定します。

data-sitekey属性にサイトキー、data-callback属性にsubmit時のコールバック関数が入ります。


resources/views/form/input.blade.php

<!doctype html>

<html lang="{{ str_replace('_', '-', app()->getLocale()) }}">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>FooBar Form</title>
<script src="https://www.google.com/recaptcha/api.js" async defer></script>
</head>
<body>
// contents

<form action="./entry/input" method="post" id="form">
+ <div class="g-recaptcha"
+ data-sitekey="{{ config('app.recaptcha.site_key') }}"
+ data-callback="onGrecaptchaSubmit"
+ data-size="invisible"></div>
// form inputs
</form>
</body>
</html>



reCAPTCHAを呼び出すためのAPI設定

コールバック関数をグローバルで定義しておきます。

また、自分の場合はJSによるバリデーションをかけていて、

reCAPTCHAのAPIを発火させるタイミングは「submitボタンが押されたとき」ではなく「バリデーションが通ったとき」にしたいので、

reCAPTCHAにより提供されているgrecaptcha.execute()関数を使用して、任意のタイミングでreCAPTCHAを実行させます。


resources/js/form-efo.js


+ window.onGrecaptchaSubmit = function(request) {
+ $('form').submit();
+ };

$(() => {
// 省略・・・

if (validated()) {
// 省略・・・
- $('form').submit();
+ grecaptcha.execute();
}
// ・・・
});


ここまででフロント側の設定は完了です。

ローカル環境で見てみると右下にreCAPTCHAのアイコンが出ているかと思います。

普通に人間が入力してsubmitするとそのまま進めますが、

自動入力などを行うと画像認証画面が出てきて引っかかるようになります。


バックエンド側の認証実装

クライアント側だけですと不正に突破される可能性もあるので、バックエンド側でも認証のチェックを行います。

簡単に言いますとクライアント側でreCAPTCHAのAPIが発火した時点でトークンが発行され、

フォームのsubmit時にg-recaptcha-responseというキーでトークンがPOSTされるので、そのトークンをバックエンド側でチェックします。


ミドルウェア作成

いろいろ方法はありますが、今回はミドルウェアを利用します。

curlを使っていますがGuzzleなどのライブラリを使用してもいいかと思います。

発行したシークレットキー・クライアント側で発行されたトークン・IPアドレスをもとに、不正なsubmitでないかをチェックします。

APIについて詳しく知りたい方は公式のドキュメント参照してみてください。

https://developers.google.com/recaptcha/docs/verify


app/Http/Middleware/CheckRecaptcha.php

<?php

namespace App\Http\Middleware;

use Closure;

class CheckRecaptcha
{
/**
* Handle an incoming request.
*
* @param \Illuminate\Http\Request $request
* @param \Closure $next
* @return mixed
*/

public function handle($request, Closure $next)
{
if ($request->input('g-recaptcha-response')){
$secret_key = config('app.recaptcha.secret_key');
$url = config('app.recaptcha.api_url');
$token = $request->input('g-recaptcha-response');

$ch = curl_init();
curl_setopt_array($ch, [
CURLOPT_URL => $url,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_POST => true,
CURLOPT_POSTFIELDS => http_build_query([
'secret' => $secret_key,
'response' => $token,
'remoteip' => $request->server->get('REMOTE_ADDR'),
]),
]);
$_response = curl_exec($ch);
curl_close($ch);

$response = json_decode($_response);

if (isset($response->success) && $response->success) {
return $next($request);
}
else {
return redirect('error'); // エラーページへリダイレクト
}
}
return redirect('error'); // エラーページへリダイレクト
}
}


ここからはLaravelだけの話なので、他のフレームワークでしたら飛ばしてオッケーです。

Kernelにミドルウェアを登録し、


app/Http/Kernel.php

    /**

* The application's route middleware.
*
* These middleware may be assigned to groups or used individually.
*
* @var array
*/
protected $routeMiddleware = [
// 省略・・・
+ 'recaptcha' => \App\Http\Middleware\CheckRecaptcha::class,
];

申し込みフォームのルーティングにミドルウェアを割り当てます。


routes/web.php

- Route::post('/entry/input', [\App\Http\Controllers\EntryInputController::class, 'input'])->name('entry.input');

+ Route::post('/entry/input', [\App\Http\Controllers\EntryInputController::class, 'input'])->name('entry.input')
+ ->middleware('recaptcha');

これでreCAPTCHAの認証設定は完了です。


補足:ログを出力する設定

バックエンドでAPI認証する際に、レスポンスの内容を出力したいということがあるかもしれません。

自分の場合は下記のようにログ出力の設定を行いました。

認証エラーが発生した場合ステータスコードと、エラー内容が分かるのでデバッグがやりやすくなるかと思います。


app/Http/Middleware/CheckRecaptcha.php

    /**

* Handle an incoming request.
*
* @param \Illuminate\Http\Request $request
* @param \Closure $next
* @return mixed
*/
public function handle($request, Closure $next)
{
if ($request->input('g-recaptcha-response')){
$secret_key = config('app.recaptcha.secret_key');
$url = config('app.recaptcha.api_url');
$token = $request->input('g-recaptcha-response');

$ch = curl_init();
curl_setopt_array($ch, [
CURLOPT_URL => $url,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_POST => true,
CURLOPT_POSTFIELDS => http_build_query([
'secret' => $secret_key,
'response' => $token,
'remoteip' => $request->server->get('REMOTE_ADDR'),
]),
]);
$_response = curl_exec($ch);
+ $_error = curl_error($ch);
curl_close($ch);

$response = json_decode($_response);
+ Log::info(
+ $_response ? [
+ 'recaptcha-result' => $response->success ? $response->success : $response['error-codes']
+ ] : ['recaptcha-error' => $_error]
+ );
+
return is_null($response) ? false : $response->success;
}
return false;
}



最後に

今回はv2非表示のセキュリティレベル1で設定しましたが、Selenium製ロボットも弾いてくれますし、ユーザーが申し込みできないといったことも今のところはありません。

比較的簡単な実装でBot対策できますので、是非やってみてください。


※参考ページ

https://developers.google.com/recaptcha/docs/invisible

日本語版が無かったので今回まとめてみました。