フォーム送信にGoogleのreCAPTCHA v3を実装する方法。
詳細が明確なreCAPTCHA v3の導入方法が見つからなかったので試作した方法のメモ。
スパム対策として、サイト内に設置したフォームからの送信をGoogleのreCAPTCHA v3で検証して、ユーザーがボットか人間かを判定する方法。(JavaScriptとPHPで実装。)
reCAPTCHAを導入するには、サイト登録と、登録後に管理コンソールの設定から取得できるreCAPTCHAのキー(「サイトキー」と「シークレットキー」)が必要。
reCAPTCHA v3は、reCAPTCHA v2のようなチェックボックスや画像認証が不要で、フォーム送信を実行したユーザーのトラフィック情報などから自動的にボットか人間かの判定を行っているが技術詳細は非公開。
v3は、サイト上の実際のトラフィックを見ることで学習していくので、実装直後の判定スコアから変動していく可能性がある。
reCAPTCHA v3の挙動の流れ
詳細はreCAPTCHAの公式ドキュメント。
[全体の流れ]
- クライアントサイドで「サイトキー」を使って「トークン」を取得。
- サーバーサイドで「シークレットキー」と「トークン」を使ってユーザーを検証。
- reCAPTCHAの検証結果をJSONデータで取得。
- 取得したJSONデータの内容を確認してユーザーを判定し、任意の処理。
[クライアントサイド]
- reCAPTCHAのJavaScript APIライブラリー(api.js)を読み込む。
- submitボタンがクリックされたら、JavaScriptでreCAPTCHAの「トークン」を取得。
- 取得した「トークン」を含むフォームの内容をサーバーサイドに渡す。
[サーバーサイド]
- クライアントサイドから「トークン」を受信。
- 「シークレットキー」と「トークン」をGoogleのreCAPTCHA APIのサーバーに送信して検証。
- reCAPTCHA APIのサーバーから返信される検証結果のJSONデータから「スコア」を確認。
- 「スコア」の値によりボットか人間かを判定して、処理の中止や本来のフォーム送信などを実行する。
※ スコアの値(0.0 ~ 1.0)は、0.0でほぼ確実にボット、1.0で最良の通信とされる。
※ サイトのトラフィックデータの蓄積によってスコアの値は改善されるので、導入からしばらくはボット判定はせずにデータを収集し、管理コンソールのデータと実際のフォーム投稿状況から、自サイトに最適なスコア判定の閾値を決めることが推奨されている。(デフォルトでは0.5を閾値とする。)
reCAPTCHA導入のGoogleによる規定
詳細は公式ドキュメントのFAQ。
1. 呼び出し制限
reCAPTCHA v3の呼び出し限度は1秒あたり1000回、または1ヶ月あたり100万回まで。
それを超える場合は、Google Cloud(従量課金制)のプロダクトのひとつで上位サービスのreCAPTCHA Enterpriseに移行するか、申請フォームから特例を申請して承認されるのを待つ。(特例の承認はケースバイケースで、非営利の場合がもっとも承認されやすい。)
- 「サイトキー」の呼び出しが毎秒1000回を超えると、いくつかのリクエストが処理されなくなる。
- v3で「サイトキー」の月の割当を超えると、0.9という静的なスコアと、当該月のリマインダーとして”Over free quota.”のエラーメッセージが返り、認証は失敗する。
- v3では月の割当を超過していても可視化されないので、ユーザーはそのことを認知できない。(v2の場合は、reCAPTCHA widgetに”This site is exceeding reCAPTCHA quota.”とメッセージが表示される。)
- ドメインに関わらず、「サイトキー」の呼び出しが月100万回を超えたら割当を超えたと見做される。(ひとつのドメインが複数の「サイトキー」を使っていても合計で適用される。)
※ 呼び出し制限の対応が適用される前にサイト所有者に3度の通知メールが送信され、reCAPTCHA Enterpriseに移行する90日間の猶予が与えられる。
2. reCAPTCHAブランドの表記
v3を導入しているページの右下にはreCAPTCHAのバッジが表示されるが、ユーザーのフォーム投稿フロー内にreCAPTCHAブランドを明示することで非表示にすることが許可されている。
公式ドキュメントでは以下の記述が求められている。
This site is protected by reCAPTCHA and the Google
<a href="https://policies.google.com/privacy">Privacy Policy</a> and
<a href="https://policies.google.com/terms">Terms of Service</a> apply.
reCAPTCHAのバッジを非表示にする場合のCSSの記述。
.grecaptcha-badge { visibility: hidden; }
3. クッキーについて
reCAPTCHAの動作には、リスクの解析のためにクッキー(「_GRECAPTCHA」)の使用が必須となっている。
4. 開発テスト運用について
ローカルな開発環境でテスト運用する場合は、ローカルドメイン(「localhost」など)専用に別途keyを発行して運用することをGoogleは推奨している。
HTMLのマークアップ
reCAPTCHAを導入するフォームが設置してあるページのHTMLの記述。
フォーム送信がクリックされたら自ページにPOSTして、reCAPTCHAの検証処理が実行されるように構築。
- フォームのPOST送信先を自ページにする。(form要素のaction属性が空だと自ページに送信される。)
- POST送信で流入してきた場合だけ「reCAPTCHAの検証処理を記述したphpファイル」を読み込む。
- HTML部分は通常のページの記述。
- reCAPTCHA APIのjsファイル(「https://www.google.com/recaptcha/api.js?render=サイトキー」)を読み込む。(grecaptchaオブジェクトのメソッドを使用するためには「render=サイトキー」のパラメーターが必要。)
- 「submitボタンのクリックイベントの処理を記述したjsファイル」を読み込む。
※ Ajaxで処理させるのが面倒だったので、自ページにそのままPOST送信して自ページをリクエストしたときにサーバーサイドでPHPでreCAPTCHA検証の処理を実行。
<?php
//POST送信での流入ならPHPファイルを読み込む(必要があればリファラーチェックなども行う)
if($_SERVER['REQUEST_METHOD'] === 'POST'){
//reCAPTCHAの検証処理を記述したphpファイルの読み込み
require '任意のファイル名.php';
}
?>
<html lang="ja">
<head>
</head>
<body>
<!-- reCAPTCHAの検証を実行した場合にエラーがあればエラーメッセージを表示 -->
<?php global $recaptcha_error; if($recaptcha_error) echo '<p>'.$recaptcha_error.'</p>'; ?>
<!-- 自ページにPOST送信するのでformのaction属性は空 -->
<form id="ag2form" action="" method="post">
<!-- 通常のフォームのフィールド -->
<input type="text" name="name" placeholder="名前">
<input type="email" name="email" placeholder="メールアドレス">
<textarea name="message" cols="50" rows="10"></textarea>
<button id="ag2submit-button" type="submit">送信</button>
</form>
<!-- reCAPTCHAのライブラリーjsファイルとsubmitボタンクリックの処理を記述したjsファイルを読み込む -->
<script src="https://www.google.com/recaptcha/api.js?render=サイトキー"></script>
<script src="/js/任意のファイル名.js"></script>
</body>
</html>
クライアントサイドの処理
[トークン]
サーバーサイドでの検証に必要となる「トークン」をクライアントサイドで取得してPOST送信する。
- トークンの取得には、reCAPTCHAのapi.jsの読み込みと「サイトキー」が必要。
- grecaptchaオブジェクトが持つメソッドでトークンの取得ができる。
- トークンの有効期限は2分間で、ひとつのトークンで1回限り検証できる。
[アクション名]
Googleはアクション名での検証を推奨しているのでアクション名も設定しておく。
- reCAPTCHAの管理コンソールで、実行されたアクションの上位10個のデータが確認できる。
- アクション名から攻撃対象となっているフォームやページを確認し、必要があれば他の対策を検討する。
- アクション名には英数字とスラッシュのみ使用できる。
- アクション名はユーザー固有のものになってはいけない。(おそらく、ユーザーごとにアクション名が変わるとどのフォームで実行されたものか分からなくなったり、すぐに10個を超えたりするため。)
JavaScriptでsubmitボタンのクリックイベント発生時に実行する処理の記述
- submitボタンのクリックで、grecaptchaオブジェクトが持つexecute()メソッドを実行してトークンを発行。(「サイトキー」と「任意のアクション名」を引数に設定する。)
- then()メソッドでトークンを受け取る。
- 動的にinput要素を生成し、name属性に「g-recaptcha-response」、value属性に「取得したトークン」を設定してform要素内に挿入。
- submit()メソッドでフォームの内容を自ページにPOST送信。
※ submitボタンに公式ドキュメント指定のクラス名(「g-recaptcha」)とdata属性(「data-callback=”関数名”」など)を付加しておくと、外部読み込みしているreCAPTCHAのapi.jsがそのコールバック関数を検出してクリック時に自動的に実行するが、コールバック関数の内容をHTML内にインラインでグローバルスコープにして記述している必要がある。それ以外の記述方法では検出できないので、自分でsubmitボタンにイベントリスナーを登録する。
const formEle = document.getElementById('ag2form'),//form要素
submitButtonEle = document.getElementById('ag2submit-button');//submitボタン
const actionName = 'CONTACT',//この検証に設定する任意のアクション名
recaptchaSitekey = 'サイトキー';
function ag2onSubmit(e){
//デフォルトの処理をキャンセル
e.preventDefault();
//トークンを取得してPOST送信
grecaptcha.ready(function(){
grecaptcha.execute(recaptchaSitekey, {action: actionName}).then(function(token){
//トークンを送信するためにinput要素を生成してform要素に挿入
inputForToken = document.createElement('input');
inputForToken.setAttribute('type', 'hidden');
inputForToken.setAttribute('name', 'g-recaptcha-response');
inputForToken.setAttribute('value', token);
formEle.appendChild(inputForToken);
//フォームのPOST送信を実行
formEle.submit();
}).catch(function(e){
//エラーで上記処理ができなかった場合
console.log('grecaptcha error: ');
console.log(e);
});
});
}
};
//submitボタンにイベントリスナーを登録
submitButtonEle.addEventListener('click', ag2onSubmit);
サーバーサイドの処理
reCAPTCHA APIによる検証結果の取得と、ユーザーがボットか人間かを判定する処理の記述。
- クライアントサイドからPOSTされた「トークン」を取得。
- 「シークレットキー」と「トークン」をGoogleのreCAPTCHA検証サーバーに送信。
- 検証結果をJSONデータで受け取る。
- 「スコア」の値を確認して分岐処理する。(ここでは閾値を0.5に設定。)
- ボットと判定した場合は、処理を終了してエラーメッセージを表示。
- 人間と判定した場合は、「本来のフォーム送信先のURL」へPOSTの内容をステータスコード307でリダイレクト。
※ HTTPステータスコード307は、リクエストされたリソースが一時的にLocationで示されたURLへ移動したことを示す。(307は302と違い、送信メソッドと本文が変更されないことが保証される。)
※ reCAPTCHA検証サーバーが返すエラーコード一覧の公式ドキュメント。
//エラーメッセージ表示用のグローバル変数
global $recaptcha_error;
//スコア判定の閾値
$score_check = 0.5;//bot 0.0 ~ 1.0 human
$recaptcha_api_url = 'https://www.google.com/recaptcha/api/siteverify';//recaptchaの検証サーバー
$secret_key = 'サイトキー';
$recaptcha_token = isset($_POST['g-recaptcha-response']) ? $_POST['g-recaptcha-response'] : null;//クライアントがPOST送信したトークン
$remote_ip = $_SERVER['REMOTE_ADDR'];//実行ユーザーのIPアドレス(必須ではない検証のオプション)
$post_action = 'CONTACT';//クライアントサイドで設定したアクション名
//recaptchaの検証結果をJSONで取得する関数
function ag2curl_recaptcha($u,$p){
$ch = curl_init();//cURLセッションを初期化
curl_setopt($ch, CURLOPT_URL, $u);//送信先URLを設定
curl_setopt($ch, CURLOPT_POST, true);//送信メソッドをPOSTにする
curl_setopt($ch, CURLOPT_POSTFIELDS, $p);//POSTのパラメーターを設定(CURLOPT_POSTFIELDSを記述する場合、CURLOPT_POSTの記述は無くても良い)
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);//データを文字列に変換して返す
curl_setopt($ch, CURLOPT_TIMEOUT, 20);//タイムアウト秒数を設定
$json = curl_exec($ch);//cURLセッションを実行
if(curl_errno($ch)){//エラー番号を返す(エラーが発生しない場合、0)
global $recaptcha_error;
$recaptcha_error = 'cURL error '.curl_errno($ch).' : '.curl_error($ch);
$json = false;
}
curl_close($ch);//cURLセッションを閉じて処理を終了
return $json;//取得したデータを返す
}
//トークンが取得できていれば検証
if($recaptcha_token){
//パラメーターを設定(シークレットキー&トークン&IPアドレス)
$para = 'secret='.$secret_key.'&response='.$recaptcha_token.'&remoteip='.$remote_ip;
//cURLを実行
$json = ag2curl_recaptcha($recaptcha_api_url, $para);
}else{
//トークンが取得できていなかった場合
$recaptcha_error = 'no token.';
$json = false;
}
if($json){
//JSON文字列をデコードして連想配列形式のオブジェクトを格納
$obj = json_decode($json, true);
//取得した検証データを変数に保持
$recaptcha_success = $obj['success'];//トークンの正否(トークンが有効かどうかで、ユーザーの判定ではない)
$recaptcha_score = $obj['score'];//検証結果のスコア
$recaptcha_action = $obj['action'];//この検証のアクション名
$recaptcha_ts = $obj['challenge_ts'];//この検証のタイムスタンプ (ISOフォーマット yyyy-MM-dd'T'HH:mm:ssZZ)
$recaptcha_hostname = $obj['hostname'];//実行されたページのホスト名
$recaptcha_error_codes = $obj['error-codes'];//エラーだった場合のエラーコード
//トークンの正否、スコア、アクション名を確認
if($recaptcha_success === true && $recaptcha_score > $score_check && $recaptcha_action === $post_action){
$redirect_url = '本来のPOST送信先URL';
//307 (Temporary Redirect)でリダイレクトして通常のフォーム投稿の処理
header('Location: '.$redirect_url, true, 307);
}else{//チェックで不正な値があった場合(ボットと判定)
$recaptcha_error = 'recaptcha Error: ';
//JSONデータのエラーコードを代入
foreach($recaptcha_error_codes as $v) {
$recaptcha_error = $recaptcha_error.'( '.$v.' )';
}
}
}
- Google reCAPTCHA の使い方(v2/v3)
- ReCAPTCHA couldn’t find user-provided function: myCallBack
- cURLでGET/POST送信する方法(PHP)
https://memo.ag2works.tokyo/post-2335/
フォーム送信にGoogleのreCAPTCHA v3を実装する方法。 | memo メモ [AG2WORKS]
<a href="https://memo.ag2works.tokyo/post-2335/" target="_blank" rel="noopener">フォーム送信にGoogleのreCAPTCHA v3を実装する方法。 | memo メモ [AG2WORKS]</a>
この記事へのコメント (3件)
お忙しいところありがとうございます。
ご指摘いただいた箇所など中心に確認してみようと思います。
突然のコメント失礼いたします。
こちらのコードを参考に既存のフォーム動作部分に手を加えず
reCAPTCHAの実装を試みているのですが、トークンが取得出来ていないのか
「no token.」が表示されてしまいます。
もし修正箇所が分かるようでしたらご教授いただけますと幸いです。
さすがにその情報だけでは何も分からないですが、
クライアントサイドの「then()」メソッドの処理段階で「token」が取得できているかコンソールに出力して確認してみてはどうでしょうか。
クライアントサイドで取得できていないなら、サイトキーが間違っているかコード記述がおかしいかだと思いますが、取得できているなら、実装したコードを見ないと何も分からないです。
あと、「既存のフォーム動作部分に手を加えず」というのが、フォーム送信のときに動作しているJavaScriptが他にも存在している可能性があるという意味なら、先に発火して送信実行している記述があるから、このトークン発行処理が発火できてない可能性もあると思います。