php Google reCAPTCHA を使ってみる(コンタクトフォーム)

2016年2月14日

コンタクトフォームで Google が提供するキャプチャ「reCAPTCHA」を使ってみたので、その使い方や設定に関するメモです。

reCAPTCHA_06

目次

reCAPTCHA を利用するサイトの登録

reCAPTCHA の API をサイトに導入するには、reCAPTCHA 公式ページでサイトを登録する必要があります。また、そのためには Google のユーザーアカウントが必要です。

ページの左側の「Create an API Key」のリンクをクリックして表示されるページで登録することができます。
reCAPTCHA_07

Google のアカウントにログイン後、以下のページで登録します。

reCAPTCHA_01

  • Label:キャプチャを識別するための名前です。自分でわかりやすい名前を付けます。
  • Domains:キャプチャを使用するドメインを指定します。
  • Owners:Google アカウントのメールアドレスを指定します。

入力後、「Register」をクリックします。入力に問題がなければ、「Site key」と「Secret key」が表示されます。

reCAPTCHA_02

  • Site Key:プログラムで使用するサイトのキーです。
  • Secret Key:プログラム(検証)で使用する秘密のキーです。(公開してはいけません)

サンプル

サンプルのコンタクトフォームは「入力ページ → 確認ページ → 完了ページ」の順で遷移し、以下の5つのファイルから成ります。

入力ページ(contact1.php)
ユーザーが「名前」「e-mail」「件名」「問い合わせ内容」を入力するページです。
スパム防止のために、画像認証機能(reCAPTCHA )を付け、以下を行います。

  • reCAPTCHA の情報(Site Key)の読み込み
  • reCAPTCHA の表示
  • jQuery での入力値の検証(reCAPTCHA の検証を含む)
確認ページ(contact2.php)
入力された値を確認・検証して、不備がある場合は、エラーを表示して再度入力フォームを表示します。
入力された値に問題がなければ、入力された値をユーザーに確認してもらい、間違いがなければ送信ボタンをクリックして送信してもらいます。以下を行います。

  • ダウンロードした reCAPTCHA の読み込み
  • PHP での入力値の検証
  • ユーザーの操作した reCAPTCHA の値($_POST[‘g-recaptcha-response’])の検証
完了ページ(contact3.php)
問い合わせの受付が完了したことを知らせるページで、入力されたデータをメールで送信し、問題がなければ送信完了のメッセージを表示します。
必要な関数をまとめて記述したファイル(functions.php)
エスケープ処理をする h() 関数、値を検証する checkInput() 関数が記述されています。
reCAPTCHA の情報のファイル(recaptchavars.php)
「Site key」と「Secret key」の値を保存するファイル(※外部からアクセスできない場所に保存する必要があります)

以下はサンプルですが、実際にメールは送信されません。
サンプル(contact1.php)

Secret Key を別ファイルに保存

Secret Key は公開しないので、外部からアクセスできない場所に保存します。この例では「recaptchavars.php」というファイルに定数として保存します。Site Key はソースコードを見ればわかってしまいますが、こちらも一緒に定数として保存しておきます。

recaptchavars.php

<?php
//reCAPTCHA サイトキー
define('RC_SITE_KEY', 'xxxxxxxxxx Site Key xxxxxxxxxxxxxxxxxxxxxxxxxx');
//reCAPTCHA シークレット
define('RC_SECRET', 'xxxxxxxxx Secret Key xxxxxxxxxxxxxxxxxx');   

reCAPTCHA のダウンロード

PHP で検証するために必要な reCAPTCHA のファイルをダウンロードします。
reCAPTCHA PHP client library のページの「Download ZIP」からダウンロードします。Composer を使ってのインストールが推奨されていますが、この例では直接ダウンロードします。

reCAPTCHA_03

解凍したファイルの中の「src」フォルダの中身が必要なファイルになるので、これを適当な場所に保存します。

reCAPTCHA_04

この例では、フォルダ名を src から ReCaptcha に変更して public_html の直下(/home/user/public_html/ReCaptcha/autoload.php)に配置します。

reCAPTCHA のウィジェットを表示

簡単にウィジェットを表示するには、以下のように head の閉じタグの直前などで reCAPTCHA の Javascript(https://www.google.com/recaptcha/api.js) を読み込み、フォーム要素内で class 属性「g-recaptcha」と data-sitekey 属性「Site Key(サイトキー)」を指定した div 要素で表示します。

<html>
  <head>
    <title>reCAPTCHA demo: Simple page</title>
     <script src="https://www.google.com/recaptcha/api.js" async defer></script>
  </head>
  <body>
    <form action="?" method="POST">
      <div class="g-recaptcha" data-sitekey="発行されたサイトキー"></div>
      <br/>
      <input type="submit" value="Submit">
    </form>
  </body>
</html>

参考:Displaying the widget

但し、上記の方法だと使い勝手が良くないので、以下のように head 内でページの読み込み(onload)時に表示するコールバック関数(onloadCallback)を指定し、HTML 部分で div 要素と script 要素を使って表示するようにします。

このサンプルでは jQuery の検証を行うため以下の3つのコールバック関数を設定しています。

  1. ページ読み込み時ウィジェットを表示するためのコールバック関数(onloadCallback)
  2. ユーザーがウィジェットにチェックをいれた場合に呼び出されるコールバック関数(verifyRecaptcha)
  3. 期限が過ぎた場合のコールバック関数(expiredCallback)
<head>
・・・中略・・・
<script type="text/javascript">
  var onloadCallback = function() {  //読み込み時のコールバック関数の指定 (1)
    grecaptcha.render('recaptcha', {  //div 要素の id 属性
      'sitekey' : "<?php echo RC_SITE_KEY; ?>",  //サイトキー
      'callback' : verifyRecaptcha,  //コールバック関数の指定 (2)
      'expired-callback' : expiredCallback  //コールバック関数の指定 (3)
    });
  };
  var verifyRecaptcha = function(response) {  // (2)
    if(response != "") { 
        //reCAPTCHA でユーザーがウィジェットにチェックをいれて成功した場合の処理     
    }
  };
  var  expiredCallback = function() {  // (3)
    //チェックをいれて成功したが期限が過ぎた場合の処理
  };
</script>
</head> 

4行目:ウィジェットを表示する onload コールバック関数(onloadCallback)を指定します。この関数の中の grecaptcha.render() の最初のパラメータではウィジェットを表示する div 要素の id 属性を指定します。

また以下のようなオプションを指定することができます。「sitekey」のみ必須です。

  • sitekey:発行されたサイトキー(必須)
  • theme :ウィジェットの色の指定 (dark または light)
  • size :ウィジェットのサイズ(compactまたは normal)
  • type:image または audio (デフォルトはimage)
  • callback:ユーザーがウィジェットを操作した時に呼び出されるコールバック関数
  • expired-callback:期限が過ぎた場合のコールバック関数

5行目:フォーム内でウィジェットを表示する div 要素の id 属性を指定します。

6行目:サイトキーの値を格納した定数 RC_SITE_KEY を使ってサイトキーを指定しています。

7行目:jQuery の検証の際に利用するコールバック関数(verifyRecaptcha)を指定します。

8行目:2分ぐらいすると自動的に無効になり再度チェックを入れるようになっているので、期限が過ぎた場合に呼び出されるコールバック関数(expiredCallback)を指定します。

11行目~15行目:コールバック関数(verifyRecaptcha)の定義です。
16行目~18行目:コールバック関数(expiredCallback)の定義です。

以下がこのサンプルのウィジェットの表示をする関数部分の記述です。

<script type="text/javascript">
  var onloadCallback = function() {
    grecaptcha.render('recaptcha', {
      'sitekey' : "<?php echo RC_SITE_KEY; ?>",
      'callback' : verifyRecaptcha,
      'expired-callback' : expiredCallback,
      'theme' : 'dark'  //ウィジェットの背景を黒に
    });
  };
  var verifyRecaptcha = function(response) {
    if(response != "") { 
        document.getElementById('recaptcha').className = "g-recaptcha verified";
        var recaptchaError = document.getElementById('recaptcha_error');
        if(recaptchaError != null) {
           recaptchaError.parentNode.removeChild(recaptchaError);
        } 
        var recaptchaExpired  = document.getElementById('expired');  
        if(recaptchaExpired != null) {
           recaptchaExpired.parentNode.removeChild(recaptchaExpired);
        }      
    }
  };
  var  expiredCallback = function() {
    var p = document.createElement('p');
    p.textContent = 'チェックボックスをもう一度オンにしてください。';
    p.setAttribute('class', 'error');
    p.setAttribute('id', 'expired');
    var recaptcha_div = document.getElementById('recaptcha');
    var recaptchaExpired  = document.getElementById('expired');
    if(recaptchaExpired == null) {
      recaptcha_div .appendChild(p);
    }   
    document.getElementById('recaptcha').className = "g-recaptcha";
  };
</script>

10行目~22行目:コールバック関数(verifyRecaptcha)の定義です。このコールバック関数は、ユーザーがウィジェットにチェックをいれて成功した場合に呼び出されます。

コールバック関数はパラメータに g-recaptcha-response の値(response)を受け取るので、それを利用します。この例では、jQuery の検証で response に何らかの値が入っていれば、reCAPTCHA の操作が行われたと想定して、reCAPTCHA を表示する div 要素(id 属性が recaptcha)にクラス属性「verified」を追加しています(g-recaptcha は予め付与されているクラスです)。そして jQuery の検証でエラーの場合に表示(追加)した要素を削除しています。(13~16行目)

また、17~20行目は期限が過ぎた場合にコールバック関数(expiredCallback)でエラーを表示した要素を削除しています。

23~34行目:期限が過ぎた場合のコールバック関数(expiredCallback)の定義です。このコールバック関数は、ユーザーがウィジェットにチェックをいれて何もしないで一定の期間が過ぎると呼び出されます。その時、ウィジェットのチェックは自動的に外れて以下のように表示されます。

reCAPTCHA_08

期限が過ぎてチェックが外れたら「チェックボックスをもう一度オンにしてください。」と表示するようにしています(ウィジェット内にもその旨表示されるので、不要かもしれません)。但し、コールバック関数(verifyRecaptcha)で追加したクラス「verified」を削除しないと、チェックが入っていないのに jQuery の検証を抜けてしまうので、33行目の処理は必要になります。

24行目~32行目は、p 要素を作成し、テキストを指定してクラス属性と id 属性を付与してウィジェットの下に表示するようにしています。

 

フォーム内にウィジェットを表示するため、form 要素内にウィジェットを表示する div 要素を記述し以下の属性を指定します。

  • id=recaptcha (head 内の onloadCallback で指定した id 属性)
  • class=g-recaptcha
  • data-sitekey=発行されたサイトキー

div 要素の data-sitekey 属性には発行されたサイトキーの値を指定します。このサンプルでは PHP の echo 文で出力しています。

<div id="recaptcha" class="g-recaptcha" data-sitekey="<?php echo RC_SITE_KEY; ?>">
</div>

そして表示するための(リソースとしての)以下の JavaScript を適当な位置(body の閉じタグの直前等)に記述します。

<script src="https://www.google.com/recaptcha/api.js?onload=onloadCallback&render=explicit" async defer></script>

onload パラメータには、head 内に記述した onload コールバック関数(この例では onloadCallback)を指定し、render パラメータには explicit を指定します。(onload=onloadCallback&render=explicit)

また、onload コールバック関数は reCAPTCHA API がロードする前に定義されている必要があります(このサンプルでは head 内で先に定義しています)。そして、上記のようにスクリプトタグ内で、async と defer パラメータを指定します。

<form action="contact2.php" method="post">  
  ・・・中略・・・

<div id="recaptcha" class="g-recaptcha" data-sitekey="<?php echo RC_SITE_KEY; ?>">
</div>
  ・・・中略・・・
</form>
<script type="text/javascript" src="https://www.google.com/recaptcha/api.js?onload=onloadCallback&render=explicit" async defer></script>
</body>

jQuery の検証では、ユーザーが何も操作していない場合(id 属性が recaptcha の div 要素に「verified」クラスが付与されていない場合)はエラーを表示します。

jQuery(function($){
  ・・・中略・・・
  if(!$('#recaptcha').hasClass('verified')) {
    $('#recaptcha').append("<p class='error' id='recaptcha_error'>チェックを入れてください</p>");
  }
  ・・・中略・・・
});
  

 

これらの設定をしたページを読み込むと以下のような通信が発生しているのが確認できます。

reCAPTCHA_09

また、ユーザーがウィジェットにチェックを入れると以下のように POST リクエストが発生しているのがわかります。

reCAPTCHA_10

ウィジェット表示のサンプル

以下は前述の内容(仕組み)をまとめたサンプルで、reCAPTCHA にチェックを入れないと送信できないようになっています。

サンプル(recaptcha_test.php)

  • reCAPTCHA にチェックを入れないで送信しようとすると「チェックを入れてください」と表示され送信できません。
  • reCAPTCHA にチェックを入れても、しばらく時間が経過して(有効期間が過ぎて)チェックが外れてしまうと「チェックボックスをもう一度オンにしてください。」と表示して送信できません。
<?php 
//reCAPTCHA の情報(Site Key)の読み込み
require_once('/home/user/etc/xxxxxx/ReCaptcha/recaptchavars.php'); 

//エスケープ処理を行う関数
function h($var) {
  return htmlspecialchars($var, ENT_QUOTES, 'UTF-8');
}
?>
<!DOCTYPE HTML>
<html lang="ja">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width">
<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1" />
<title>コンタクト</title>
<link rel="stylesheet" href="style.css">
<script type="text/javascript">
  var onloadCallback = function() {
    grecaptcha.render('recaptcha', {
      'sitekey' : "<?php echo RC_SITE_KEY; ?>",
      'callback' : verifyRecaptcha,
      'expired-callback' : expiredCallback
    });
  };
  var verifyRecaptcha = function(response) {
    if(response != "") { 
        document.getElementById('recaptcha').className = "g-recaptcha verified";
        var recaptchaError = document.getElementById('recaptcha_error');
        if(recaptchaError != null) {
           recaptchaError.parentNode.removeChild(recaptchaError);
        } 
        var recaptchaExpired  = document.getElementById('expired');  
        if(recaptchaExpired != null) {
           recaptchaExpired.parentNode.removeChild(recaptchaExpired);
        }      
    }
  };
  var  expiredCallback = function() {
    var p = document.createElement('p');
    p.textContent = 'チェックボックスをもう一度オンにしてください。';
    p.setAttribute('class', 'error');
    p.setAttribute('id', 'expired');
    var recaptcha_div = document.getElementById('recaptcha');
    var recaptchaExpired  = document.getElementById('expired');
    if(recaptchaExpired == null) {
      recaptcha_div .appendChild(p);
    }   
    document.getElementById('recaptcha').className = "g-recaptcha";
  };
</script>
</head>
<body>
<div id="contents">

<h2> reCAPTCHA サンプル </h2>
<form action="" method="post">
<!-- reCAPTCHA -->
  <div id="recaptcha" class="g-recaptcha" data-sitekey="<?php echo RC_SITE_KEY; ?>"></div>
 <!-- reCAPTCHA -->
  <p><input class="btn_submit" type="submit" value="送信"></p>
</form>
</div>
<!-- reCAPTCHA -->
<script type="text/javascript" src="https://www.google.com/recaptcha/api.js?onload=onloadCallback&render=explicit" async defer></script>
<!-- reCAPTCHA -->
<script src="http://code.jquery.com/jquery-1.11.3.min.js"></script>
<script type="text/javascript">

jQuery(function($){
  
  $("form").submit(function(){  //submit イベント
    //エラー表示の初期化
    $("p.error").remove();
       
    //reCAPTCHA の検証
    if(!$('#recaptcha').hasClass('verified')) {
      $('#recaptcha').append("<p class='error' id='recaptcha_error'>チェックを入れてください</p>");
    }   
     
    //エラーがあれば送信しない
    if($("p.error").size() > 0){
      return false;
    }
  }) 
 
});
</script>
</body>
</html>

コンタクトフォームへの reCAPTCHA の実装

以下が contact1.php の全文です。

contact1.php

<?php
//セッションを開始
session_start();   

//セッションIDを変更(セッションハイジャック対策)
session_regenerate_id(TRUE);  

//reCAPTCHA の情報(Site Key)の読み込み
require_once('/home/user/etc/xxxx/ReCaptcha/recaptchavars.php'); 

//データの検証とエスケープ処理の関数の読み込み
require 'libs/functions.php';   

//初回以外ですでにセッション変数に値が代入されていれば、その値を。そうでなければNULLで初期化
$name = isset($_SESSION['name']) ? $_SESSION['name'] : NULL; 
$email = isset($_SESSION['email']) ? $_SESSION['email'] : NULL;
$subject= isset($_SESSION['subject']) ? $_SESSION['subject'] : NULL;
$body = isset($_SESSION['body']) ? $_SESSION['body'] : NULL;
$error = isset($_SESSION['error']) ? $_SESSION['error'] : NULL;

//CSRF対策の固定トークンを生成
if(!isset($_SESSION['ticket'])){
  //セッション変数にトークンを代入
  $_SESSION['ticket'] = sha1(uniqid(mt_rand(), TRUE));
}

//トークンを変数に代入
$ticket = $_SESSION['ticket'];

?>
<!DOCTYPE HTML>
<html lang="ja">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width">
<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1" />
<title>コンタクト</title>
<link rel="stylesheet" href="style.css">
<!-- reCAPTCHA -->
<script type="text/javascript">
  var onloadCallback = function() {
    grecaptcha.render('recaptcha', {
      'sitekey' : "<?php echo RC_SITE_KEY; ?>",
      'callback' : verifyRecaptcha,
      'expired-callback' : expiredCallback,
      'theme' : 'dark'
    });
  };
  var verifyRecaptcha = function(response) {
    if(response != "") { 
        document.getElementById('recaptcha').className = "g-recaptcha verified";
        var recaptchaError = document.getElementById('recaptcha_error');
        if(recaptchaError != null) {
           recaptchaError.parentNode.removeChild(recaptchaError);
        } 
        var recaptchaExpired  = document.getElementById('expired');  
        if(recaptchaExpired != null) {
           recaptchaExpired.parentNode.removeChild(recaptchaExpired);
        }      
    }
  };
  var  expiredCallback = function() {
    var p = document.createElement('p');
    p.textContent = 'チェックボックスをもう一度オンにしてください。';
    p.setAttribute('class', 'error');
    p.setAttribute('id', 'expired');
    var recaptcha_div = document.getElementById('recaptcha');
    var recaptchaExpired  = document.getElementById('expired');
    if(recaptchaExpired == null) {
      recaptcha_div .appendChild(p);
    }   
    document.getElementById('recaptcha').className = "g-recaptcha"; 
  };
</script>
<!-- reCAPTCHA -->
</head>
<body>
<div id="contents">

<h2>コンタクト </h2>
<div id="errorDispaly">
<?php if(isset($error)): ?>
<?php foreach($error as $val): ?>
<?php echo h($val); ?><br />
<?php endforeach; ?>
<?php endif; ?>
</div><!--end of #errorDispaly-->

<div id="formArea">
<fieldset id="contactForm">
<legend class="contact_form">お問い合わせフォーム</legend>
<form action="contact2.php" method="post">
<div class="form-group">
  <label for="name">お名前</label>
  <input type="text" name="name" value="<?php echo h($name); ?>" size="50" placeholder="お名前(必須)" class="validate required max30">
</div>
<div class="form-group">  
  <label for="email">e-mail</label>
  <input type="text" name="email" value="<?php echo h($email); ?>" size="50" placeholder="メールアドレス(必須)" class="validate required mail">
</div>
<div class="form-group"> 
  <label for="subject">件名</label>
  <input type="text" name="subject" value="<?php echo h($subject); ?>" size="50" placeholder="件名(必須)" class="validate required max50">
</div>
<div class="form-group">  
  <label for="body">内容</label> <span id="count"> </span>/500
  <textarea name="body" cols="50" rows="8" placeholder="内容(必須:500文字まで)" class="validate required max500"><?php echo h($body); ?></textarea>
</div>
<div class="form-group"><!-- reCAPTCHA -->
  <div id="recaptcha" class="g-recaptcha" data-sitekey="<?php echo RC_SITE_KEY; ?>"></div>
  <noscript><!-- JavaScript が無効な場合にエラーを表示  -->
    <p class="error">JavaSrcipt を有効にしてください。JavaSrcipt が無効の場合、このフォームは機能しません</p>
  </noscript>
</div>  
  <p>
  <input class="btn_submit" type="submit" value="確認画面へ">
  </p>
  <!--確認ページへトークンをPOSTする、隠しフィールド「ticket」-->
  <input type="hidden" name="ticket" value="<?php echo h($ticket); ?>">
</form>
</fieldset>
</div><!--end of #formArea-->
</div><!--end of #contents--> 
<script type="text/javascript" src="https://www.google.com/recaptcha/api.js?onload=onloadCallback&render=explicit" async defer></script>
<script src="http://code.jquery.com/jquery-1.11.3.min.js"></script>
<script type="text/javascript">

jQuery(function($){
  
  function show_error(message, this$) {
    text = this$.parent().find('label').text() + message;
    this$.parent().append("<p class='error'>" + text + "</p>")
  }
  
  //submit イベントを利用して値を検証
  $("form").submit(function(){  
    //エラー表示の初期化
    $("p.error").remove();
    $("div").removeClass("error");
    var text = "";
    
    //reCAPTCHA の検証
    if(!$('#recaptcha').hasClass('verified')) {
      $('#recaptcha').append("<p class='error' id='recaptcha_error'>チェックを入れてください</p>");
    }
    
    //1行テキスト入力フォームとテキストエリアの検証
    $(":text,textarea").filter(".validate").each(function(){
        
      //必須項目の検証
      $(this).filter(".required").each(function(){
        if($(this).val()==""){
          show_error("は必須項目です", $(this));
        }
      })
      //文字数の検証
      $(this).filter(".max30").each(function(){
        if($(this).val().length > 30){
          show_error("は30文字以内です", $(this));
        }
      })
      //文字数の検証
      $(this).filter(".max50").each(function(){
        if($(this).val().length > 50){
          show_error("は50文字以内です", $(this));
        }
      })
      //文字数の検証
      $(this).filter(".exact6").each(function(){
        if($(this).val() !=="" && $(this).val().length !== 6){
          show_error("は6文字です", $(this));
        }
      })
      //文字数の検証
      $(this).filter(".max500").each(function(){
        if($(this).val().length > 500){
          show_error("は500文字以内です", $(this));
        }
      })
      //メールアドレスの検証
      $(this).filter(".mail").each(function(){
        if($(this).val() && !(/^([a-z0-9\+_\-]+)(\.[a-z0-9\+_\-]+)*@([a-z0-9\-]+\.)+[a-z]{2,6}$/gi).test($(this).val())){
          $(this).parent().append("<p class='error'>メールアドレスの形式が異なります</p>")
        }
      }) 
    })
     
    //error クラスの追加の処理
    if($("p.error").size() > 0){  //エラーがあれば
      $("p.error").parent().addClass("error");  //親要素にクラスを追加
      //最初の error クラスがついた p 要素へスクロール
      $('html,body').animate({ scrollTop: $("p.error:first").offset().top-40 }, 'slow');  
      return false;  //送信をキャンセル
    }
  }) //ここまでが submit イベントを使った検証
  
  //テキストエリアに入力された文字数を表示
  $("textarea").on('keydown keyup change', function() {
    var count = $(this).val().length;
    $("#count").text(count);
    if(count > 500) {
      $("#count").css({color: 'red', fontWeight: 'bold'});
    }else{
      $("#count").css({color: '#333', fontWeight: 'normal'});
    }
  });
 
});
</script>
</body>
</html>

以下は、jQuery の検証用(エラーの表示用)の CSS です。

p.error {
  color: red;
}
.error input , 
.error textarea {
    background-color: #F9E9E9;
}

参考ページ:Displaying the widget

jQuery の検証が不要な場合

このサンプルでは、jQuery の検証を付けているので少し複雑になっていますが、jQuery の検証が不要な場合は以下のようにウィジェットを表示する関数を指定するだけでとてもシンプルになります。

但し、入力内容に誤りがあった場合には再度ウィジェットにチェックを入れなければならない等使い勝手が落ちてしまいます。

<?php
//セッションを開始
session_start();   

//セッションIDを変更(セッションハイジャック対策)
session_regenerate_id(TRUE);  

//reCAPTCHA の情報(Site Key)の読み込み
require_once('/home/user/etc/xxxx/ReCaptcha/recaptchavars.php'); 

//データの検証とエスケープ処理の関数の読み込み
require 'libs/functions.php';   

//初回以外ですでにセッション変数に値が代入されていれば、その値を。そうでなければNULLで初期化
$name = isset($_SESSION['name']) ? $_SESSION['name'] : NULL; 
$email = isset($_SESSION['email']) ? $_SESSION['email'] : NULL;
$subject= isset($_SESSION['subject']) ? $_SESSION['subject'] : NULL;
$body = isset($_SESSION['body']) ? $_SESSION['body'] : NULL;
$error = isset($_SESSION['error']) ? $_SESSION['error'] : NULL;

//CSRF対策の固定トークンを生成
if(!isset($_SESSION['ticket'])){
  //セッション変数にトークンを代入
  $_SESSION['ticket'] = sha1(uniqid(mt_rand(), TRUE));
}

//トークンを変数に代入
$ticket = $_SESSION['ticket'];

?>
<!DOCTYPE HTML>
<html lang="ja">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width">
<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1" />
<title>コンタクト</title>
<link rel="stylesheet" href="style.css">
<!-- reCAPTCHA -->
<script type="text/javascript">
  var onloadCallback = function() {
    grecaptcha.render('recaptcha', {
      'sitekey' : "<?php echo RC_SITE_KEY; ?>",
      'theme' : 'dark'
    });
  };
</script>
<!-- reCAPTCHA -->
</head>
<body>
<div id="contents">

<h2>コンタクト </h2>
<div id="errorDispaly">
<?php if(isset($error)): ?>
<?php foreach($error as $val): ?>
<?php echo h($val); ?><br />
<?php endforeach; ?>
<?php endif; ?>
</div>

<div id="formArea">
<fieldset id="contactForm">
<legend class="contact_form">お問い合わせフォーム</legend>
<form action="contact2.php" method="post" >
<div class="form-group">
  <label for="name">お名前</label>
  <input type="text" name="name" value="<?php echo h($name); ?>" size="50" placeholder="お名前(必須)" class="validate required max30 form-control input_name">
</div>
<div class="form-group">  
  <label for="email">e-mail</label>
  <input type="text" name="email" value="<?php echo h($email); ?>" size="50" placeholder="メールアドレス(必須)" class="validate required mail form-control input_email">
</div>
<div class="form-group"> 
  <label for="subject">件名</label>
  <input type="text" name="subject" value="<?php echo h($subject); ?>" size="50" placeholder="件名(必須)" class="validate required max50 form-control input_subject">
</div>
<div class="form-group">  
  <label for="body">内容</label> <span id="count"> </span>/500
  <textarea name="body" cols="50" rows="8" placeholder="内容(必須:500文字まで)" class="validate required max500 form-control input_body"><?php echo h($body); ?></textarea>
</div>
<div class="form-group">
  <!-- reCAPTCHA -->
  <div id="recaptcha" class="g-recaptcha" data-sitekey="<?php echo RC_SITE_KEY; ?>"></div>
  <noscript>
    <p class="error">JavaSrcipt を有効にしてください。JavaSrcipt が無効の場合、このフォームは機能しません。</p>
  </noscript>
  <!-- reCAPTCHA -->
</div>  
  <p>
  <input class="btn_submit" type="submit" value="確認画面へ">
  </p>
  <!--確認ページへトークンをPOSTする、隠しフィールド「ticket」-->
  <input type="hidden" name="ticket" value="<?php echo h($ticket); ?>">
</form>
</fieldset>
</div>
</div> 
<!-- reCAPTCHA -->
<script type="text/javascript" src="https://www.google.com/recaptcha/api.js?onload=onloadCallback&render=explicit" async defer></script>
<!-- reCAPTCHA -->
<script src="http://code.jquery.com/jquery-1.11.3.min.js"></script>
</body>
</html>

PHP での検証

contact2.php では、ユーザーが操作して送信された値が正しいかの検証をします。

Site Key と Secret Key の情報を読み込み、続いてダウンロードして保存した「autoload.php」を読み込みます。

そして reCAPTCHA のインスタンスを生成します。new のパラメータには発行された Secret Key を指定します。(この例では、recaptchavars.php に定数で定義されています)

//reCAPTCHA の情報(Site Key と Secret Key)の読み込み
require_once('/home/user/etc/xxxx/ReCaptcha/recaptchavars.php'); 

//reCAPTCHA の読み込み
require_once('/home/user/public_html/ReCaptcha/autoload.php'); 
$recaptcha = new \ReCaptcha\ReCaptcha(RC_SECRET);  //インスタンスを生成

ユーザーの操作した結果の値を検証するには、以下のようにします。

<?php
$recaptcha = new \ReCaptcha\ReCaptcha(シークレットキー);  //インスタンスを生成
$resp = $recaptcha->verify(reCAPTCHA のレスポンス, リモートIPアドレス);
if ($resp->isSuccess()) {
    // 認証された場合
} else {
    $errors = $resp->getErrorCodes();
}

この例では、認証されなかった場合にエラーの内容を配列 $error[] に代入します。

ユーザーの操作の結果は $_POST[‘g-recaptcha-response’] に格納されています。$_SERVER[‘REMOTE_ADDR’] はユーザーの IP アドレスで、オプションです。

//画像認証(reCAPTCHA)のチェック
$resp = $recaptcha->verify($_POST['g-recaptcha-response'], $_SERVER['REMOTE_ADDR']);
if (!$resp->isSuccess()) {
  foreach ($resp->getErrorCodes() as $code) {
    $error[] = 'reCAPTCHA エラー : ' . $code ;
  }
}

通常ユーザーがウィジェットにチェックを入れて、jQuery の検証を通過していればこのエラーが表示されることはありませんが、もし jQuery の検証をしない場合は、エラーメッセージをもう少し工夫したほうが良いと思います。

ユーザーが全ての項目を入力し、ウィジェットにチェックを入れて「確認画面へ」のボタンをクリックすると contact2.php へデータが POST されます。

reCAPTCHA_11

参考ページ:reCAPTCHA PHP client library

上記参考ページは reCAPTCHA の「Introduction」ページの「Code Examples」の下の「PHP」からアクセスできます。

以下は、contact2.php の全文です。

<?php
session_start();    //セッションを開始

require 'libs/functions.php';   //データの検証とエスケープ処理の関数の読み込み

//reCAPTCHA の情報の読み込み
require_once('/home/user/etc/xxxx/ReCaptcha/recaptchavars.php'); 

//reCAPTCHA の読み込み
require_once('/home/user/public_html/ReCaptcha/autoload.php'); 
$recaptcha = new \ReCaptcha\ReCaptcha(RC_SECRET);

//POSTされたデータをチェック
$_POST = checkInput($_POST);

//固定トークンを確認(CSRF対策)
if(isset($_POST['ticket'], $_SESSION['ticket'])){
  $ticket = $_POST['ticket'];
  if($ticket !== $_SESSION['ticket']){
    die('不正アクセスの疑いがあります。');
  }
}else{
  die('不正アクセスの疑いがあります。');
}

//POSTされたデータを変数に格納
$name = isset($_POST['name']) ? $_POST['name'] : NULL;
$email = isset($_POST['email']) ? $_POST['email'] : NULL;
$subject = isset($_POST['subject']) ? $_POST['subject'] : NULL;
$body = isset($_POST['body']) ? $_POST['body'] : NULL;
//以下は画像認証用データ
$recaptcha_response = isset($_POST['g-recaptcha-response']) ? $_POST['g-recaptcha-response'] : NULL;  


//POSTされたデータを整形(前後にあるホワイトスペースを削除)
$name = trim($name);
$email = trim($email);
$subject = trim($subject);
$body = trim($body);
$recaptcha_response = trim($recaptcha_response);

//エラーメッセージを保存する配列の初期化
$error = array();

if($name == '') {
  $error[] = '*お名前は必須項目です。';
  //制御文字でないことと文字数をチェック
}else if(preg_match('/\A[[:^cntrl:]]{1,30}\z/u', $name) == 0) {   
  $error[] = '*お名前は30文字以内でお願いします。';
}

if($email == ''){
  $error[] = '*メールアドレスは必須です。';
}else{   //メールアドレスを正規表現でチェック
  $pattern = '/^([a-z0-9\+_\-]+)(\.[a-z0-9\+_\-]+)*@([a-z0-9\-]+\.)+[a-z]{2,6}$/uiD';
  if(!preg_match($pattern, $email)){
    $error[] = '*メールアドレスの形式が正しくありません。';
  }
}

if($subject == '') {
  $error[] = '*件名は必須項目です。';
  //制御文字でないことと文字数をチェック
}else if(preg_match('/\A[[:^cntrl:]]{1,100}\z/u', $subject) == 0) {   
  $error[] = '*件名は100文字以内でお願いします。';
}

if($body == '') {
  $error[] = '*内容は必須項目です。';
  //制御文字(タブ、復帰、改行を除く)でないことと文字数をチェック
}else if(preg_match('/\A[\r\n\t[:^cntrl:]]{1,500}\z/u', $body) == 0) {   
  $error[] = '*内容は500文字以内でお願いします。';
}

//画像認証(reCAPTCHA)のチェック
$resp = $recaptcha->verify($_POST['g-recaptcha-response'], $_SERVER['REMOTE_ADDR']);
if (!$resp->isSuccess()) {
  foreach ($resp->getErrorCodes() as $code) {
    $error[] = 'reCAPTCHA エラー : ' . $code ;
  }
}

//POSTされたデータをセッション変数に保存
$_SESSION['name'] = $name;
$_SESSION['email'] = $email;
$_SESSION['subject'] = $subject;
$_SESSION['body'] = $body;
$_SESSION['error'] = $error;

//チェックの結果にエラーがあった場合は入力フォームに戻します
if(count($error) >0){    //エラーがあった場合
  $dirname = dirname($_SERVER['SCRIPT_NAME']);
  $dirname = $dirname == DIRECTORY_SEPARATOR ? '' : $dirname;
  $url = 'http://' . $_SERVER['SERVER_NAME']. $dirname . '/contact1.php';
  header('HTTP/1.1 303 See Other');
  header('location: ' . $url); 
}else{    //エラーがなかった場合
  
}
?>

<!DOCTYPE HTML>
<html lang="ja">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width">
<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1" />
<title>コンタクト(確認)</title>
<link rel="stylesheet" href="style.css">
</head>
<body>
<div id="contents">
<h2>コンタクト(確認)</h2>
<div id="confirmArea">
<p>以下の内容でよろしければ送信ボタンを押してください。</p>
  <dl>
  <dt>お名前:</dt>
  <dd><?php echo h($name); ?></dd>
  <dt>e-mail:</dt>
  <dd><?php echo h($email); ?></dd>
  <dt>件名:</dt>
  <dd><?php echo h($subject); ?></dd>
  <dt>内容:</dt>
  <dd><?php echo nl2br(h($body)); ?></dd>
  </dl>
  </div>
  <fieldset class="confirm">
<form action="contact1.php" method="post">
  <p><input type="submit" value="入力画面へ戻る" class="btn_submit"></p>
</form>
</fieldset>
<fieldset class="confirm">
<form action="contact3.php" method="post">
  <input type="hidden" name="ticket" value="<?php echo h($ticket); ?>">
  <p><input type="submit" value="送信する" class="btn_submit"></p>
</form>
</fieldset>

</div><!--end of #contents--> 
<script src="http://code.jquery.com/jquery-1.11.3.min.js"></script>
</body>
</html>

その他のページでは、reCAPTCHA に関する設定はありませんが参考までに。

contact3.php

<?php
session_start();    //セッションを開始

require 'libs/functions.php';   //データの検証とエスケープ処理の関数の読み込み

//POSTされたデータをチェック
$_POST = checkInput($_POST);

//固定トークンを確認(CSRF対策)
if(isset($_POST['ticket'], $_SESSION['ticket'])){
  $ticket = $_POST['ticket'];
  if($ticket !== $_SESSION['ticket']){
    die('不正アクセスの疑いがあります。');
  }
}else{
  die('不正アクセスの疑いがあります。');
}

//変数にセッション変数を代入
$name = $_SESSION['name'];
$email = $_SESSION['email'];
$subject = $_SESSION['subject'];
$body = 'コンタクトページからの問い合わせ'. "\n\n" . h($_SESSION['body']);

//--------sendmail------------

//メールの宛先
$mailTo = 'info@example.com';

//Return-Pathに指定するメールアドレス
$returnMail = 'info@example.com';

//mbstringの日本語設定
mb_language('ja');
mb_internal_encoding('UTF-8');

//From ヘッダーを作成
$header = 'From: ' . mb_encode_mimeheader($name). ' <' . $email. '>';

//メールの送信、セーフモードがOnの場合は第5引数が使えない
/*if(ini_get('safe_mode')){
  $result = mb_send_mail($mailTo, $subject, $body, $header);
}else{
  $result = mb_send_mail($mailTo, $subject, $body, $header, '-f'. $returnMail);
}*/
$result = true; //サンプルなのでメールの送信はしない。(上記コメントアウト)

//送信結果により表示するメッセージの変数を初期化
$message = '';

//メール送信の結果判定
if($result) {
  $message = 'ありがとうございます。送信完了いたしました。<br>(実際には送信されていません)';
  //成功した場合はセッションを破棄
  $_SESSION = array();   //空の配列を代入し、すべてのセッション変数を消去 
  session_destroy();   //セッションを破棄
}else{
  $message = '申し訳ございませんが、送信に失敗しました。しばらくしてもう一度お試しになるか、メールにてご連絡ください。';
}

?>
<!DOCTYPE HTML>
<html lang="ja">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width">
<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1" />
<title>コンタクト(完了)</title>
<link rel="stylesheet" href="style.css">
</head>
<body>
<div id="contents">
<h2>コンタクト(<?php echo $result ? "完了" : "送信失敗"; ?>)</h2>
<div id="message">
<p><?php
if($result) {
	echo '<h3>送信完了!</h3>';
}else{
	echo '<h3><a href="contact1.php">送信失敗</a></h3>';
}
?></p>
<p><?php echo h($message); ?></p>
</div><!--end of #message-->
<?php if(!$result): ?>
<p>送信失敗が継続する場合は、一度ブラウザを閉じてからやり直すとうまくいくことがあります。</p>
<?php endif; ?>

</div><!--end of #contents--> 
</body>
</html>

functions.php

<?php
//エスケープ処理を行う関数
function h($var) {
  if(is_array($var)){
    //$varが配列の場合、h()関数をそれぞれの要素について呼び出す(再帰)
    return array_map('h', $var);
  }else{
    return htmlspecialchars($var, ENT_QUOTES, 'UTF-8');
  }
}

//入力値に不正なデータがないかなどをチェックする関数
function checkInput($var){
  if(is_array($var)){
    return array_map('checkInput', $var);
  }else{
    //php.iniでmagic_quotes_gpcが「on」の場合の対策
    if(get_magic_quotes_gpc()){  
      $var = stripslashes($var);
    }
    //NULLバイト攻撃対策
    if(preg_match('/\0/', $var)){  
      die('不正な入力です。');
    }
    //文字エンコードのチェック
    if(!mb_check_encoding($var, 'UTF-8')){ 
      die('不正な入力です。');
    }
    //改行、タブ以外の制御文字のチェック
    if(preg_match('/\A[\r\n\t[:^cntrl:]]*\z/u', $var) === 0){  
      die('不正な入力です。制御文字は使用できません。');
    }
    return $var;
  }
}

JavaScript を OFF にしている場合

JavaScript を OFF にしている場合は、ウィジェットは表示されません。またこのサンプルの場合、PHP でユーザー操作のレスポンスを検証しているので、フォームを送信することができません。

そのため、JavaScript を OFF にしている場合には、noscript 要素で JavaScript を有効にする必要があることを伝える等するか、iframe を利用した noscript 要素を記述する必要があります。

reCAPTCHA の「Frequently asked questions」の「Does reCAPTCHA support users that don’t have JavaScript enabled?」に以下の noscript 要素が紹介されています。(ウィジェットを表示する要素のすぐ後に記述します)

<script src="https://www.google.com/recaptcha/api.js" async defer></script>
<div class="g-recaptcha" data-sitekey="your_site_key"></div>
<noscript>
  <div style="width: 302px; height: 422px;">
    <div style="width: 302px; height: 422px; position: relative;">
      <div style="width: 302px; height: 422px; position: absolute;">
        <iframe src="https://www.google.com/recaptcha/api/fallback?k=your_site_key"
            frameborder="0" scrolling="no"
            style="width: 302px; height:422px; border-style: none;">
        </iframe>
      </div>
      <div style="width: 300px; height: 60px; border-style: none;
            bottom: 12px; left: 25px; margin: 0px; padding: 0px; right: 25px;
            background: #f9f9f9; border: 1px solid #c1c1c1; border-radius: 3px;">
        <textarea id="g-recaptcha-response" name="g-recaptcha-response"
              class="g-recaptcha-response"
              style="width: 250px; height: 40px; border: 1px solid #c1c1c1;
              margin: 10px 25px; padding: 0px; resize: none;" >
        </textarea>
      </div>
    </div>
  </div>
</noscript>

このサンプルで上記コードを記述した場合、ユーザーが認証用コードを入力するテキストエリア部分がうまく表示されなかったので、ウィジェットを表示する div 要素のスタイルの「position: absolute;」を削除して、外側の div 要素の高さをテキストエリアの高さ分(60px)増やして「 482px」にする必要がありました。

以下が、iframe を利用した noscript 要素を追加した場合のフォーム要素の例です。iframe の src 属性の最後にサイトキーを指定する必要があります。

<form action="contact2.php" method="post">
<div class="form-group">
  <label for="name">お名前</label>
  <input type="text" name="name" value="<?php echo h($name); ?>" size="50" placeholder="お名前(必須)" class="validate required max30 form-control input_name">
</div>
・・・中略・・・
<div class="form-group">
  <div id="recaptcha" class="g-recaptcha" data-sitekey="<?php echo RC_SITE_KEY; ?>"></div>
<noscript><!--ここから-->
  <div style="width: 302px; height: 482px;">
    <div style="width: 302px; height: 422px; position: relative;">
      <div style="width: 302px; height: 422px;">
        <iframe src="https://www.google.com/recaptcha/api/fallback?k=<?php echo RC_SITE_KEY; ?>"
          frameborder="0" scrolling="no"
          style="width: 302px; height:422px; border-style: none;">
        </iframe>
      </div>
      <div style="width: 300px; height: 60px; border-style: none;
           bottom: 12px; left: 25px; margin: 0px; padding: 0px; right: 25px;
           background: #f9f9f9; border: 1px solid #c1c1c1; border-radius: 3px;">
        <textarea id="g-recaptcha-response" name="g-recaptcha-response"
           class="g-recaptcha-response"
           style="width: 250px; height: 40px; border: 1px solid #c1c1c1;
           margin: 10px 25px; padding: 0px; resize: none;" >
        </textarea>
      </div>
    </div>
  </div>
</noscript><!--ここまで-->
</div>  
  <p>
  <input class="btn_submit" type="submit" value="確認画面へ">
  </p>
  <!--確認ページへトークンをPOSTする、隠しフィールド「ticket」-->
  <input type="hidden" name="ticket" value="<?php echo h($ticket); ?>">
</form>

但し、 reCAPTCHA の「Frequently asked questions」には「If JavaScript is a requirement for your site, we advise that you do NOT include this.」とあり、サイトで JavaScript を必要とする場合は、このコードを記述しないことを推奨するとなっています。(使い勝手が悪いためのようです)

このサンプルでは単に noscript 要素で JavaScript を有効にする必要があることを表示するようにしています。

対応ブラウザ

Browser requirements for reCAPTCHA」に記載されています。
以下のブラウザの最新の2バージョンをサポートするとなっています。

desktop (Windows, Linux, Mac)

  • Chrome
  • Firefox
  • Safari
  • IE

mobile

  • Chrome
  • Safari
  • Android native browser (4.0+)

また、IE8~IE10 の場合、互換表示設定に google.com が含まれているとうまく表示されないことがあるようです。(reCAPTCHA isn’t displaying properly on Internet Explorer, what do I do?

参考:「reCAPTCHA Help

一度認証したら、reCAPTCHA を再表示させない

確認画面から入力画面に戻った場合(一度 reCAPTCHA の認証が行われた場合)に reCAPTCHA を表示させない場合の例。(これで問題がないのかは不明。一応機能するようですが、もっと別の方法があるかも知れません。)

サンプル(contact1.php)

「contact1.php」の変更点

// reCAPTCHA の認証が行われたかどうかを追加
$recaptcha_verified = isset($_SESSION['recaptcha_verified']) ? $_SESSION['recaptcha_verified'] : NULL;   

// reCAPTCHA の値「Site key」を変数に代入
$siteKey = RC_SITE_KEY;  //Added  

head 内の reCAPTCHA の JavaScript 部分を PHP で条件により出力

<?php
if (!$recaptcha_verified)
echo <<<EOT
<script type="text/javascript">
  var onloadCallback = function() {
    grecaptcha.render('recaptcha', {
      'sitekey' : "{$siteKey}",
      'callback' : verifyRecaptcha,
      'expired-callback' : expiredCallback,
      'theme' : 'dark'
    });
  };
  var verifyRecaptcha = function(response) {
    if(response != "") { 
        document.getElementById('recaptcha').className = "g-recaptcha verified";
        var recaptchaError = document.getElementById('recaptcha_error');
        if(recaptchaError != null) {
           recaptchaError.parentNode.removeChild(recaptchaError);
        } 
        var recaptchaExpired  = document.getElementById('expired');  
        if(recaptchaExpired != null) {
           recaptchaExpired.parentNode.removeChild(recaptchaExpired);
        }      
    }
  };
  var  expiredCallback = function() {
    var p = document.createElement('p');
    p.textContent = 'チェックボックスをもう一度オンにしてください。';
    p.setAttribute('class', 'error');
    p.setAttribute('id', 'expired');
    var recaptcha_div = document.getElementById('recaptcha');
    var recaptchaExpired  = document.getElementById('expired');
    if(recaptchaExpired == null) {
      recaptcha_div .appendChild(p);
    }   
    document.getElementById('recaptcha').className = "g-recaptcha";
  };
</script>
EOT
?>

reCAPTCHA の HTML を PHP で条件により出力

<?php
if (!$recaptcha_verified)
echo <<<EOT
<div class="form-group">
  <div id="recaptcha" class="g-recaptcha" data-sitekey="<?php echo RC_SITE_KEY; ?>"></div>
</div>
EOT
?>

「contact2.php」の変更点

// reCAPTCHA の認証が行われたかどうかを追加
$recaptcha_verified = isset($_SESSION['recaptcha_verified']) ? $_SESSION['recaptcha_verified'] : NULL; 

//画像認証(reCAPTCHA)のチェック *一度認証したらチェックしないように変更。
if(!$recaptcha_verified) {
  $resp = $recaptcha->verify($_POST['g-recaptcha-response'], $_SERVER['REMOTE_ADDR']);
  if (!$resp->isSuccess()) {
    foreach ($resp->getErrorCodes() as $code) {
      $error[] = 'reCAPTCHA エラー : ' . $code ;
    }
  } else {
    $recaptcha_verified = true;
  }
} else {
  $recaptcha_verified = true;
}

//認証されているかどうかをセッション変数に保存
$_SESSION['recaptcha_verified'] = $recaptcha_verified;  

関連ページ:「コンタクトフォームの作成