#052
posted on 2021.08.09

フォーム送信にGoogleのreCAPTCHA v3を実装する方法。

詳細が明確なreCAPTCHA v3の導入方法が見つからなかったので試作した方法のメモ。

スパム対策として、サイト内に設置したフォームからの送信をGoogleのreCAPTCHA v3で検証して、ユーザーがボットか人間かを判定する方法。(JavaScriptとPHPで実装。)

 

reCAPTCHAを導入するには、サイト登録と、登録後に管理コンソールの設定から取得できるreCAPTCHAのキー(「サイトキー」と「シークレットキー」)が必要。

reCAPTCHA v3は、reCAPTCHA v2のようなチェックボックスや画像認証が不要で、フォーム送信を実行したユーザーのトラフィック情報などから自動的にボットか人間かの判定を行っているが技術詳細は非公開。

v3は、サイト上の実際のトラフィックを見ることで学習していくので、実装直後の判定スコアから変動していく可能性がある。

 

reCAPTCHA v3の挙動の流れ

詳細はreCAPTCHAの公式ドキュメント

 

[全体の流れ]

  1. 「サイトキー」を使ってクライアントサイドで「トークン」を取得。
  2. 「シークレットキー」と「トークン」を使ってサーバーサイドで検証データをJSONで取得。
  3. 取得したJSONデータを確認してユーザーを判定。

 

[クライアントサイド]

  1. reCAPTCHAのJavaScript APIライブラリー(api.js)を読み込む。
  2. submitボタンがクリックされたら、JavaScriptでreCAPTCHAの「トークン」を取得。
  3. 取得した「トークン」を含むフォームの内容をサーバーサイドに渡す。

[サーバーサイド]

  1. クライアントサイドから「トークン」を受信。
  2. 「シークレットキー」と「トークン」をGoogleのreCAPTCHA APIのサーバーに送信して検証。
  3. reCAPTCHA APIのサーバーから返信される検証結果のJSONデータから「スコア」を確認。
  4. 「スコア」の値によりボットか人間かを判定して、処理の中止や本来のフォーム送信などを実行する。

※ スコアの値(0.0 ~ 1.0)は、0.0でほぼ確実にボット、1.0で最良の通信とされる。

※ サイトのトラフィックデータの蓄積によってスコアの値は改善されるので、導入からしばらくはボット判定はせずにデータを収集し、管理コンソールのデータと実際のフォーム投稿状況から、自サイトに最適なスコア判定の閾値を決めることが推奨されている。(デフォルトでは0.5を閾値とする。)

 

 

reCAPTCHA導入のGoogleによる規定

詳細は公式ドキュメントのFAQ

 

呼び出し制限

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日間の猶予が与えられる。

 

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; }

 

クッキーについて

reCAPTCHAの動作には、リスクの解析のためにクッキー(「_GRECAPTCHA」)の使用が必須となっている。

 

開発テスト運用について

ローカルな開発環境でテスト運用する場合は、ローカルドメイン(「localhost」など)専用に別途keyを発行して運用することをGoogleは推奨している。

 

 

HTMLのマークアップ

reCAPTCHAを導入するフォームが設置してあるページのHTMLの記述。

  1. フォームのPOST送信先を自ページにする。(form要素のaction属性が空だと自ページに送信される。)
  2. POST送信で流入してきた場合だけ「reCAPTCHAの検証処理を記述したphpファイル」を読み込む。
  3. HTML部分は通常のページの記述。
  4. reCAPTCHA APIのjsファイル(「https://www.google.com/recaptcha/api.js?render=サイトキー」)を読み込む。(grecaptchaオブジェクトのメソッドを使用するためには「render=サイトキー」のパラメーターが必要。)
  5. 「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>
</body>
<!-- reCAPTCHAのライブラリーjsファイルとsubmitボタンクリックの処理を記述したjsファイルを読み込む -->
<script src="https://www.google.com/recaptcha/api.js?render=サイトキー"></script>
<script type="text/javascript" src="/js/任意のファイル名.js"></script>
</html>

 

 

クライアントサイドの処理

[トークン]

サーバーサイドでの検証に必要となるトークンをクライアントサイドで取得してPOST送信する。

  • トークンの取得には、reCAPTCHAのapi.jsの読み込みと「サイトキー」が必要。
  • grecaptchaオブジェクトが持つメソッドでトークンの取得ができる。
  • トークンの有効期限は2分間で、ひとつのトークンで1回限り検証できる。

[アクション名]

Googleはアクション名での検証を推奨しているのでアクション名も設定する。

  • reCAPTCHAの管理コンソールで、実行されたアクションの上位10個のデータが確認できる。
  • アクション名から攻撃対象となっているフォームやページを確認し、必要があれば他の対策を検討する。
  • アクション名には英数字とスラッシュのみ使用できる。
  • アクション名はユーザー固有のものになってはいけない。(おそらく、ユーザーごとにアクション名が変わるとどのフォームで実行されたものか分からなくなったり、すぐに10個を超えたりするため。)

 

JavaScriptでsubmitボタンのクリックイベント発生時に実行する処理の記述。

  1. submitボタンのクリックで、grecaptchaオブジェクトのexecute()メソッドを実行してトークンを発行。(「サイトキー」と「任意のアクション名」を引数に設定する。)
  2. then()メソッドでトークンを受け取る。
  3. 動的にinput要素を生成し、name属性に「g-recaptcha-response」、value属性に「取得したトークン」を設定してform要素内に挿入。
  4. 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による検証結果の取得と、ユーザーがボットか人間かを判定する処理の記述。

  1. クライアントサイドからPOSTされたトークンを取得。
  2. 「シークレットキー」と「トークン」をGoogleのreCAPTCHA検証サーバーに送信。
  3. 検証結果をJSONデータで受け取る。
  4. スコアの値を確認して分岐処理する。(ここでは閾値を0.5に設定。)
  5. ボットと判定した場合は、処理を終了してエラーメッセージを表示。
  6. 人間と判定した場合は、本来のフォーム送信先の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 ag2recaptchaCurl($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);//パラメーターを設定
  curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);//データを文字列に変換して返す
  curl_setopt($ch, CURLOPT_TIMEOUT, 20);//タイムアウト秒数を設定
  $json = curl_exec($ch);//cURLセッションを実行
  if(curl_errno($ch)){//エラー番号を返す(エラーが発生しない場合、0)
    $recaptcha_error = 'curl error: '.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 = ag2recaptchaCurl($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.' )';
		}
	}
}

 

 

この記事をシェア

この記事へのコメント

コメントの書き込みはまだありません。

  • コメント内のタグはエスケープ処理され、文字列として出力されます。
  • セキュリティーのため、投稿者のIPアドレスは取得されます。
  • 管理者が内容を不適切と判断したコメントは削除されます。
  • このフォームにはスパム対策として、Googleの提供するreCAPTCHAシステムが導入されています。
    (Google Privacy Policy and Terms of Service.)