こんにちはjunです。皆さんはフォームを作成する時にBot・スパム対策を行っていますか?フォームというのは少し知識があれば、簡単にbot的に送信することができます。
curlでPOSTすることもあれば、javascriptを実行して機械的にフォームを送信することがきるので、スパム(嫌がらせ)やサーバーへの過剰負荷の原因となります。
この様な機械的な操作を防ぐために、よく「ロボットではありません」「画像に表示されている文字を入力してください」みたいなbotでは簡単に処理できないものを用意します。
しかしこの様な機能は自分で実装するのは大変です。そんな時に便利なのがreCAPTCHAです。
reCAPTCHAはGoogleが無料で提供しているBot対策ツールです。現在v3までリリースされており、v2は画像を選択させたりチェックを入れるといったユーザーのアクションでbotかどうかを検証します。v3はその様なアクションを必要とせず、必要なスクリプトを入れるだけで検証ができます。これからの実装の場合はv3を入れることをお勧めします。
今回の解説ではv3でのフロントエンドの実装とバックエンドの実装を解説していきます。そして利用するreCAPTCHAはエンタープライズではなく、無料のものを利用します。バックエンドにはLaravel6(php)を用いて説明します。バックエンドは基本的にやることはどの言語・フレームワークでも特に差異はありません。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を有効にするためのスクリプトを読み込みます。そして一部フォームを編集します。
<!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のコントローラーでの記述を想定しています。バリデーションなどは各自設定してください。
バックエンドで行うことは
以上となります。コードは以下の通りです。
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を想定しますが、他のフレームワークや言語でもやることは特に変わりません。上手くご自身の環境に置き換えてください。