Google reCAPTCHA V3をPHPのメールフォームへ実装するメモ

PHP
この記事は約10分で読めます。

この記事は、4日前に書かれました。

はじめに

スクラッチで作成したPHPメールフォームにGoogle reCAPTCHAを実装した事例は多数ありますが、V2とV3では実装方法が微妙に異なります。
今回はV3を実装した際の備忘録です。

V2とV3の違い

V2の特長は「私はロボットではありません」のチェックボックスをクリックさせたり、画像認証(信号機・横断歩道を選ぶ等)を表示して、人間であることを確認する仕組みですが、V3ではユーザー操作を邪魔せず、ページ閲覧や操作の行動パターンをスコア化して「人間かボットか」を判定する仕組みとなっています。
スコアは0~1の範囲となり、1に近いほど人間、0に近いほどボットの操作となります。
このスコアを用いてPHP側で判定するのですが、閾値(しきい値)は0.5を標準として、場合によっては0.3や0.4を用いることもあります。
この辺りの実装例も解説します。

キーの発行

reCaptchaを使用するには、まずはキーを発行する必要があります。
以下の公式サイトから発行できますが、Googleアカウントは先に作成しておきましょう。

Sign in - Google Accounts

以下の画面より発行します。
今回はV3を使用しますので、reCAPTCHAタイプは「スコアべース(v3)」を選択します。
その他、ラベルや設置するドメインなどを入力して送信します。

サイトキーとシークレットッキーの2つが発行されますので、コピーしておきます。
シークレットキーのほうは外部に漏らさないようにしましょう。

HTML側の実装

<head>~</head>の中に以下のコードを追加します。
先に発行したサイトキーをここで使用します。render=の後ろに追加してください。

<script src="https://www.google.com/recaptcha/api.js?render=発行されたサイトキー></script>

<form>~</form>タグ内のいずれかの場所に以下のコードを追加します。

<input type="hidden" name="g-recaptcha-response" id="recaptchaResponse">

送信ボタンの<button>タグにidを設定しておきます。今回はsubmit-btnとしておきました。

<button type="submit" id="submit-btn">確認</button>

ページ下部の</body>の直前に以下のJavascriptを入れます。
こちらのJavascriptでは何を行っているかと言いますと、reCAPTCHA V3ではトークンの有効期限が2分間しかありませんので、フォームに入力している間にタイムオーバーとなってしまうため、送信後にトークンが無効になってしまいます。
それを防ぐために、送信ボタンをクリックした時点でトークンを発行し、POST送信を実行するようにしています。
actionはsubmitとしていますが、こちらも任意の文字列で構いません。ただし、PHP側の判定ロジックでも合わせる必要がありますので注意してください。

<script>
const form = document.getElementById('contact-form');
const submitBtn = document.getElementById('submit-btn');

form.addEventListener('submit', function(e) {
	e.preventDefault(); 
	grecaptcha.ready(function() {
	grecaptcha.execute('発行されたサイトキーを入れる', {action: 'submit'}).then(function(token) {
		document.getElementById('recaptchaResponse').value = token;
		form.submit(); 
	});
	});
});
</script>

画面右下にこのようなステッカーが表示されたら入力画面側の実装は完了です。

PHP側の判定処理

固定値をrecapthca.ini.phpファイルとして設定しておきます。

define('GR_SITE_KEY', '発行されたサイトキーを入れる');
define('GR_SECRET_KEY', '発行されたシークレットキーを入れる');
define('GR_SCORE', 0.5);

PHP側にて判定するための処理を実装します。今回はクラス化して使いまわす例となります。
受け取る引数はシークレットキー、Googleからのレスポンス、閾値の3つを想定しています。
POSTリクエストかどうか、reCAPTHCAレスポンスが存在するかなどを先にチェックしています。
他にもAPIサーバーと通信が失敗した場合なども一応入れておきました。

class checkRecaptchaV3{

	public $message;
	public $judge_score;

	function __construct( $secret_key, $recaptcha_response, $score=0.5 ){

		$this->message  = '';

		// POSTリクエストかどうか、トークンが存在するかを確認
		if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($recaptcha_response) && isset($secret_key)) {

			// GoogleのAPIに検証リクエストを送信
			$recaptcha_url = 'https://www.google.com/recaptcha/api/siteverify';
			$recaptcha_data = [
				'secret'   => $secret_key,
				'response' => $recaptcha_response,
				'remoteip' => $_SERVER['REMOTE_ADDR'], // ユーザーのIPアドレスを送信(推奨)
			];

			$options = [
				'http' => [
					'method'  => 'POST',
					'header'  => 'Content-type: application/x-www-form-urlencoded',
					'content' => http_build_query($recaptcha_data),
					'timeout' => 7, // タイムアウトを7秒に設定
				],
			];
			$context  = stream_context_create($options);

			$response_json = @file_get_contents($recaptcha_url, false, $context);


			if ($response_json === false) {
				// APIサーバーとの通信自体に失敗した場合
				$this->message = "reCAPTCHAサーバーとの通信に失敗しました。サイト管理者にお問い合わせください。";

			}else{

				//  APIからのレスポンスをデコード
				$response_data = json_decode($response_json);

				if ($response_data === null) {
					// JSONのデコードに失敗した場合
					$this->message = "reCAPTCHAサーバーから無効な応答がありました。サイト管理者にお問い合わせください。";

				}else{

					// APIが成功応答(success: true)を返さなかった場合のハンドリング
					if (!isset($response_data->success) || $response_data->success !== true) {
						$this->message = "reCAPTCHAの認証に失敗しました。時間をおいて再度お試しください。";
						
						// 失敗理由を追記
						if (isset($response_data->{'error-codes'})) {
							$this->message .= '|reCAPTCHA verification failed. Error codes: ' . implode(', ', $response_data->{'error-codes'});
						}

					}else{

						//  検証結果を確認
						if ($response_data && $response_data->success) {
							// スコアとアクションを検証
							// スコアの閾値は0.5を基準に、必要に応じて調整
							$this->judge_score = $response_data->score;
							if ($response_data->score >= $score && $response_data->action == 'submit') {
								// ----- 検証成功 -----
			
								$this->message = "";
			
							} else {
								// スコアが低い、またはアクションが一致しない場合
								$this->message = "reCAPTCHAの認証に失敗しました。ボットの可能性があります。";
							}
						} else {
							// APIとの通信に失敗、または無効なトークン
							$this->message = "reCAPTCHAの認証に失敗しました。時間をおいて再度お試しください。";
						}
					}

				}
        
			}
			

		} else {
			// 不正なアクセス
			$this->message = "フォームから正しく送信してください。";
		}
	}

}

クラスの使用方法は以下のようになります。
エラー処理はサンプルですので、送信を停止してエラーメッセージを出力するなり、入力画面へ戻すなりしてください。
5行目は、スコア判定を確認するためにサンプルとして入れています。

require_once('recaptcha.ini.php');

$rcp = new checkRecaptchaV3(GR_SECRET_KEY, $_POST['g-recaptcha-response'], GR_SCORE);

echo '<p>reCaptcha判定スコア:'.$rcp->judge_score.'|しきい値:'.GR_SCORE.'</p>';

if($rcp->message){
	echo $rcp->message;
	exit;			
}

スコアを確認してみる

実際に送信してみて、スコアがどのようになるか確認してみます。
画面上に出力するようにしてみました。
手入力の場合は0.9となりました。

次に、Katalon Recorderにて自動テストで実行してみます。

こちらも予想に反して0.9となりました。なぜ?

まとめ

wordpressを使用している場合は、reCAPTCHA V3の実装は簡単だと思いますが、スクラッチで何らかの送信フォーム系を作成している場合は手動で実装する必要があります。
入力画面側のJavascriptはもう少しエラー処理を入れる必要があります。
以上、reCAPTCHA V3の実装例でした。