PHPセキュリティ対策:PHPでreCAPTCHAを使う

(1/1)
reCAPTCHAロゴ
reCAPTCHA は、ボットがサイトの入力画面からデータ入力するのを防ぐための技術で、2007年(平成19年)に米カーネギー大学が開発し、2009年(平成21年)にGoogleが買い取った。
みなさんも、サイトの入力画面に左図のロゴが表示されるのをご覧になったことがあるだろう。当サイトでも、「メッセージ受付」画面で reCAPTCHA を利用している。
今回は、PHPを使い、このメッセージ受付画面と同じものを作ってみることにする。

サンプル・プログラムの実行例

PHPでreCAPTCHAを使う
reCAPTCHA を利用する入力画面は、右下にロゴが表示される。
reCAPTCHAは、この画面で「送信」ボタンをクリックするのがボットではなく、人間であることを判定する。

目次

サンプル・プログラムのダウンロード

圧縮ファイルの内容
SendMessage.phpサンプル・プログラム

準備:reCAPTCHAの利用登録

reCAPTCHAの利用登録
まず、Google reCAPTCHA から利用登録を行う。

reCAPTCHA にはV2とV3があるが、今回は利用者に負担をかけることが少ないV3を使用することにする。
また、Googleアカウントがない方は、事前にアカウントを入手してほしい。

Google reCAPTCHAの右上にある「Admin console」をクリックする。初回は左図のような画面が表示されるので、必要事項を選択し、reCAPTCHA を使用するドメインを入力し、「送信」を押下する。
reCAPTCHAの利用登録
登録が完了すると、左図の画面に移動する。
サイトキーシークレットキー は、これから作るプログラムで利用するので、記録しておく。

  38: //チェックするURLパターン
  39: define('PAT_URL', "/^https?\:\/\/[a-z\.]*pahoo\.org/ui");
  40: 
  41: //送信先メールアドレス
  42: define('MAIL_TO', '*************');
  43: 
  44: //Google reCAPTCHA v3
  45: //https://www.google.com/recaptcha/intro/v3.html から無償入手
  46: //サイトキー
  47: define('SITE_KEY', '*************');
  48: //シークレットキー
  49: define('SECRET_KEY', '*************');
  50: //トークンネーム(固定)
  51: define('TOKEN_NAME', '*************');

入手したサイトキーシークレットキー は、それぞれ定数の格納しておく。

今回のプログラムでは、問い合わせ内容をサイト管理者のメールアドレスへ送信する。送信先メールアドレスは定数 MAIL_TO に、問い合わせURLが自サイトかどうかを判別するための正規表現パターンを定数 PAT_URL に、それぞれ定義しておく。

reCAPTCHAの仕組み

reCAPTCHAの仕組み
reCAPTCHA ウィジェットはGoogleからJavaScriptの形で提供されている。これを組み込んだフォームから、サイトキーを使って Google reCAPTCHA サーバにトークンを要求する。
取得したトークンと、その他入力データをセットにしてスコア判定プログラム(サーバサイド)に送る。判定プログラムは入手したトークンと、シークレットキーを使い、Google reCAPTCHA サーバにトークン検査を要求する。すると、そのトークンに対応するスコアが返される。スコアの価は0.0以上1.0以下で、おおむね0.5未満はbotの可能性がある。

Google reCAPTCHA サーバがどのような方法でスコア計算しているかは明らかにされていないが、当サイトでは、この reCAPTCHA を導入することで、迷惑メッセージが皆無になったことから、結果として効果を発揮している。

プログラムの流れ

PHPでreCAPTCHAを使う
プログラムは、メッセージ入力から reCAPTCHA によるスコア判定、送信完了画面まで、すべて1本のPHPプログラムを使って行い、パラメータによって表示する画面を切り替える。

入力チェック画面

 311: /**
 312:  * HTML BODYを作成する - 入力チェック用
 313:  * @param   string $name    ニックネーム
 314:  * @param   string $url     関連URL
 315:  * @param   string $message メッセージ
 316:  * @param   bool   $private 非公開フラグ
 317:  * @return  string HTML BODY
 318: */
 319: function makeBodyCheck($name, $url, $message, $private) {
 320:     $myself = MYSELF;
 321: 
 322:     //入力チェック
 323:     $flag = TRUE;
 324:     if ($name == '') {
 325:         $flag = FALSE;
 326:         $name_msg = '<span class="red">※ニックネームは必須です.入力してください.</span>';
 327:     } else {
 328:         $name_msg = '';
 329:     }
 330: 
 331:     
 332:     if (($title = checkURL($url)) == FALSE) {
 333:         $flag = FALSE;
 334:         $url_msg = '<span class="red">※当サイトのURLではありません.再入力してください.</span>';
 335:     } else {
 336:         $url_msg = '';
 337:     }
 338: 
 339:     if ($message == '') {
 340:         $flag = FALSE;
 341:         $message_msg = '<span class="red">※メッセージは必須です.入力してください.</span>';
 342:     } else {
 343:         $message_msg = '';
 344:     }
 345: 
 346:     //非公開フラグの処理
 347:     $private_msg = $private ? 'メッセージは非公開にする.' : 'メッセージは公開する.';
 348:     $private = $private ? '<input type="hidden" name="private" value="TRUE" />' : '';
 349: 
 350:     //ボタン作成
 351:     $button = $flag ? '<input type="submit" name="send" value="送信" />' : '';
 352: 
 353: //reCAPTCHA v3対応フォーム
 354: $site_key   = SITE_KEY;
 355: $token_name = TOKEN_NAME;
 356: $body =<<< EOD
 357: <script src="https://www.google.com/recaptcha/api.js?render={$site_key}"></script>
 358: <script>
 359: grecaptcha.ready(function() {
 360:     grecaptcha.execute('{$site_key}',
 361:         {action:'contact'}).then(function(token) {
 362:         var token_name = document.getElementById('{$token_name}');
 363:             token_name.value = token;
 364:     });
 365: });
 366: </script> 
 367: <!-- タイトル -->
 368: <h1 class="index1">お問い合わせ内容確認</h1>
 369: 
 370: <!-- 本文 -->
 371: <div class="main">
 372: <p>
 373: ご意見、ご質問、励ましのメッセージを当サイト管理者へ送信します。<br />
 374: 下記の内容で問題なければ、「送信」ボタンをクリックしてください。
 375: </p>
 376: 
 377: <p>
 378: ●ニックネーム {$name_msg}
 379: <blockquote class="quote2">{$name}</blockquote>
 380: </p>
 381: <p>
 382: ●関連URL {$url_msg}<span class="blue">{$title}</span>
 383: <blockquote class="quote2">{$url}</blockquote>
 384: </p>
 385: <p>
 386: ●メッセージ {$message_msg}
 387: <blockquote class="quote2">{$message}</blockquote>
 388: ●{$private_msg}
 389: </p>
 390: <form name="form_message" id="form_message" method="post" action="{$myself}">
 391: <div align="center">
 392:  <input type="submit" name="input" id="input" value="入力画面に戻る" /> 
 393: {$button}
 394: </div>
 395: <input type="hidden" name="name" id="name" value="{$name}" />
 396: <input type="hidden" name="url"  id="url"  value="{$url}" />
 397: <input type="hidden" name="message" id="message" value="{$message}" />
 398: <input type="hidden" name="{$token_name}" id="{$token_name}" />
 399: {$private}
 400: </form>
 401: 
 402: <p align="center">
 403: <a href="/" class="footer1 style1">トップページへ戻る</a>
 404: </p>
 405: </div>
 406: </body>
 407: 
 408: EOD;
 409:     return $body;
 410: }

データ入力画面を生成するユーザー関数 makeBodyInput からのデータを受け取り、データ内容を検査するとともに、reCAPTCHA にトークン要求をするユーザー関数が makeBodyChec である。
前半で、入力データの検査を行う。

中盤(表示HTMLのbodyタグの冒頭)に、reCAPTCHA にトークン要求をするJavaScriptを記述する。
取得したトークンは定数 TOKEN_NAME で定義されたオブジェクトに格納され、次の画面に渡される。

reCAPTCHA v3 APIからデータ取得

 222: /**
 223:  * reCAPTCHA v3 APIからデータ取得
 224:  * @param   string $token トークン
 225:  * @return  array  応答データ
 226: */
 227: function get_reCAPTCHA($token) {
 228:     $secret_key = SECRET_KEY;
 229: 
 230:     $ch = curl_init();
 231:     curl_setopt($ch, CURLOPT_URL, 'https://www.google.com/recaptcha/api/siteverify');
 232:     curl_setopt($ch, CURLOPT_POST, TRUE);
 233: 
 234:     //API パラメータの指定
 235:     curl_setopt($ch, CURLOPT_POSTFIELDS, http_build_query(array(
 236:         'secret' => $secret_key,
 237:         'response' => $token
 238:     )));
 239:     curl_setopt($ch, CURLOPT_RETURNTRANSFER, TRUE);
 240: 
 241:     $api_response = curl_exec($ch);
 242:     curl_close($ch);
 243: 
 244:     return json_decode($api_response);
 245: }

先ほど取得したトークンとシークレットキーを使って reCAPTCHA に問い合わせを行うユーザー関数が get_reCAPTCHA である。

Google reCAPTCHA は、入力パラメータ(IN)は POST 渡しで、出力結果(OUT)は JSON で戻るというWebAPIである。
WebAPIのURL
URL
https://www.google.com/recaptcha/api/siteverify

入力パラメータ
フィールド名 要否 内  容
secret 必須 シークレットキー
response 必須 トークン
応答データ(json) success トークンの真偽(TRUE|FALSE) score スコア(0.0以上1.0以下) action アクション名 challenge_ts タイムスタンプ hostname ホスト名 error-codes エラーメッセージ

送信完了

 520: //処理分岐
 521: if (isset($_POST['check'])) {
 522:     $HtmlBody = makeBodyCheck($name, $url, $message, $private);
 523: else if (isset($_POST['send'])) {
 524:     $res = get_reCAPTCHA($token);
 525:     if (isset($res->success&& $res->success && isset($res->score&& ($res->score >0.5)) {
 526:         $HtmlBody = makeBodySend($name, $url, $message, $private);
 527:     } else {
 528:         $HtmlBody = makeBodyFail($name, $url, $message, $private);
 529:     }
 530: else {
 531:     $HtmlBody = makeBodyInput($name, $url, $message, $private);
 532: }
 533: 
 534: // 表示処理
 535: echo $HtmlHeader;
 536: echo $HtmlBody;
 537: echo $HtmlFooter;

前述のユーザー関数 get_reCAPTCHA で取得したスコアが0.5以上であれば、ユーザー関数 makeBodySend を実行し、メッセージのメール送信と送信完了画面を表示する。
そうでなければ、ユーザー関数 makeBodyFail を実行し、メール送信せずに、送信失敗したことをユーザーに通知する。
前半で、入力データの検査を行う。

参考サイト

(この項おわり)
header