PHP Logo クッキー / セッション / コンタクトフォーム

更新日:2018年07月21日

セッションの利用

変数はページがリロードされると、その際にクリアされてしまいますが、セッションを利用することでセッション変数の中に入れたデータは、ブラウザのページを更新しても、別のページへ移動しても保持することができます。

セッションとは、ユーザーがページを移動している間、そのユーザーのアクセス情報をそのユーザーごとに保持したまま移動できる仕組みです。

PHP のセッションは、ユーザーを識別する「セッション ID」と Web サーバーにデータを保存する「セッション変数($_SESSION)」により実現されます。

クッキーはデータをユーザーのブラウザに保存しますが、セッションはデータ(セッション変数)を Web サーバー上に保存し、セッション ID のみをクッキー(セッションクッキー)に保存します。

参考:PHP マニュアル セッション 基本的な使用法

クッキーの問題点

クッキーを使った情報の保持には、以下のような問題点があります。

データがクライアント側で保存される
クッキーで保存されたデータは、クライアント側で自由に削除したり変更することが可能なため、クッキーの情報を元にアプリケーション全体の挙動を左右するような判定を行うのは問題があります。
実データがネットワーク上を流れる
通信系路上にリクエスト情報をロギングするような通信機器やソフトウェアがあると、クッキー情報が漏洩する可能性があります。

セッションは以下が、クッキーと異なります。

  • データがサーバー側で保存される。
  • ネットワーク上を流れるのは、セッション ID のみ。

session_start() 命令

セッションを利用するには、ページの先頭で、session_start() 関数を実行します。

session_start()関数は、必ず、Webブラウザへの出力が行われる前に、呼び出す必要があります。(setcookie()関数と同様、HTTPプロトコルの制約)

session_start() 関数を呼び出した後は、スーパーグローバル変数の1つであるセッション変数「$_SESSION」が使えるようになり、データを保存することができます。

セッション変数は、セッションを終了するまでページを移動しても利用することが可能です。$_SESSION に値を格納すると、同一のセッションで値を共有できます。(この変数は,同じ名前でもセッションごとに値が異なります。)

また、セッションは、session_start() が記述されているページでのみセッション変数「$_SESSION」を利用することができます。

デフォルトの設定でセッションを開始したときは,Web ブラウザを閉じるまで同じセッション(ID)が有効で Web ブラウザを閉じるとセッションは終了します。

標準ではセッション変数は、セッションファイルに保存され、保存場所は「php.ini」の「session.save_path」で指定されています。

以下が、session_start() 関数の構文です。

bool session_start ([ $options ] )

パラメータ

$options(array):オプションの連想配列を指定することができます。これは、現在設定されている セッションの設定ディレクティブを上書きします。 連想配列のキーにはプレフィックス session. を含めてはいけません。

戻り値

セッションが正常に開始した場合に TRUE、それ以外の場合に FALSE を返すします。

session_start() 関数は、初めてのアクセスか、2回目以降のアクセスかを判別します。

初めてのアクセスの場合は、「セッション ID」生成します。そうでない場合は、セッションクッキーまたは GET, POST により渡された「セッション ID」 に基づき現在のセッションを復元します。

以下は、セッションを使った簡単なカウンタの例です。

初回アクセス時には、セッション変数 $_SESSION['count'] がまだ存在しないので、その場合は $_SESSION['count'] に「1」をセットします。

セッション変数 $_SESSION['count'] がすでに存在する場合は、値を「1」増やします(++)。

最後に echo 文で、現在のアクセス数を表示します。その際にセッション変数をエスケープ処理して出力します。

<?php
//セッション開始
session_start();
?>
<!doctype html>
<html lang="ja">
<head>
<meta charset="utf-8">
<title>セッションを使った簡単なカウンタ</title>
</head>
<body>
<?php
if(!isset($_SESSION['count'])){
  $_SESSION['count'] = 1;    //初回のアクセス
}else{
  $_SESSION['count'] ++;    //2回目以降のアクセス
}
//変数を出力する際はエスケープ処理します
echo "<p>アクセス回数: " . h($_SESSION['count']) . "</p>";

//エスケープ処理を行う関数 h() 
function h($str) {
  return htmlspecialchars($str, ENT_QUOTES, 'UTF-8');
}
?> 
</body>
</html>

session_counter.php

セッション変数「$_SESSION」は、通常の変数と同じように配列データも代入することができます。以下は2つのページで、セッションが渡されているのを確認する例です。

<?php 
//セッション開始
session_start();
?>
<!doctype html>
<html lang="ja">
<head>
<meta charset="utf-8">
<title>セッションのサンプル1</title>
</head>
<body>
<?php
$_SESSION['test'] = array('php' => 'PHP: Hypertext Preprocessor', 
                          'html' => 'Hyper Text  Markup Language',
                          'ajax' => 'Asynchronous  JavaScript And XML',
                          'css' => 'Cascading Style Sheet');
?> 
<p><a href="session_sample2.php">session_sample2.php(確認ページへ)</a></p>
</body>
</html>

session_sample1.php

<?php
//セッション開始
session_start();
?>
<!doctype html>
<html lang="ja">
<head>
<meta charset="utf-8">
<title>セッションのサンプル2</title>
</head>
<body>
<?php
if(isset($_SESSION['test'])) {
  foreach($_SESSION['test'] as $key => $value) {
    //値の出力はエスケープ処理します。
    echo "$key :  ". h($value). "<br>";
  }
}else{
  echo "Session はありません。";
}
//エスケープ処理を行う関数 h() 
function h($str) {
  return htmlspecialchars($str, ENT_QUOTES, 'UTF-8');
}
?> 
<p><a href="session_sample1.php">session_sample1.php(戻る)</a></p>
</body>
</html>

session_sample2.php

最初に「session_sample1.php」にアクセスし、その後リンクから「session_sample2.php」にアクセスして、登録したセッション情報が表示されれば成功です。

セッションの流れ

以下では Firefox の Firebug で前述の session_counter.php を使ってセッション ID を確認してみます。

Firebug を起動して、「ネット」タブを選択します。

最初のアクセスでは、リクエストヘッダーにセッションクッキーがセットされていないので、Web サーバーは「セッション ID」生成します。

Web サーバーからのレスポンスヘッダーに「Set-Cookie」と言う項目があり、「PHPSESSID=l8clhs9sv7a682dos0637qu0g1; path=/」となっていて、これがセッションクッキーです。

「l8clhs9sv7a682dos0637qu0g1」の部分が「セッション ID」で、サーバーがランダムに生成する数字とアルファベットからなる文字列(32桁)です。

「PHPSESSID」は、セッション名(セッションIDを渡す際の変数名)でデフォルトではこのように PHPSESSID になっています。セッション名は、session_name() 関数でセットすることもできます。

このレスポンスヘッダーを受け取ったブラウザは、「PHPSESSID」という名前のセッションクッキーを保存します。

Web サーバはセッション ID を元に、$_SESSION 変数に格納された値を保持するためのファイルを Web サーバ上に生成します。(この時点でファイルの中身は空です。)

そして PHP スクリプトの実行の完了とともに、$_SESSION 変数に格納された値を Web サーバ上の生成されたセッションファイルに書き込みます。($_SESSION 変数に格納された値はネットワーク上を流れません)

セッションの確認(レスポンスヘッダー)

続いて、「Cookie」タブを選択してみます。

セッションクッキーの名前(PHPSESSID)と内容(l8clhs9sv7a682dos0637qu0g1)が確認できます。

セッションクッキーの確認

続いて、ブラウザのリロードボタンをクリックして、ページを再読み込みします。

2回目のアクセスでは、ブラウザからのリクエストヘッダーに「Cookie」という項目が表示され、ブラウザからセッション ID「PHPSESSID=l8clhs9sv7a682dos0637qu0g1」が Web サーバーに送信されているのがわかります。

セッション ID がクッキーによって送信されると、session_start() 関数は Web サーバー上にセッションファイルがあるかどうか確認し、ファイルが存在した場合は、ファイルの中身を取り出して、$_SESSION の値を復活させます。

セッションの確認

続いて、「Cookie」タブを選択してみます。

セッションクッキーの名前(PHPSESSID)と内容(l8clhs9sv7a682dos0637qu0g1)が同じことが確認できます。

セッションクッキーの確認

参考サイト:
10日で覚えるPHPのキソ 第 10 回 セッション(SESSION)
第8回 セッションの仕組みを知ろう (その1)

セッション名の取得・設定

セッション名を取得または設定するには、session_name() 関数を使用します。

以下が構文です。

string session_name([ $name ] )

パラメータ

  • $name(string):セッションの名前。セッション名は英数字のみで構成されている必要があり、また数字だけで構成することはできず、少なくとも1文字以上の英字が必要です。

デフォルトのセッション名は「PHPSESSID」です。

この値はセッションクッキーのキーにも使われます。$_COOKIE[session_name()]

戻り値

その時点でのセッションの名前を返します。

session_name() は、パラメータを指定せず実行すると現在のセッション名を返します。

パラメータ name を渡すと、session_name() は現在のセッション名を上書きして元のセッション名を返します。

リクエストが開始される際にセッション名はリセットされ、 session.name(php.ini) に保存されたデフォルト値に戻ります。 よって、各リクエスト毎に session_start() を呼び出す前に session_name() を呼び出す必要があります。

以下はセッション名を変更して、元のセッション名と変更後のセッション名を表示する例です。

<?php
//セッション名を MYSESSION に変更
$default_session_name = session_name('MYSESSION');
session_start();
?>
<!doctype html>
<html lang="ja">
<head>
<meta charset="utf-8">
<title>セッション名の変更</title>
</head>
<body>
<?php
echo "<p>デフォルトのセッション名: " . $default_session_name . "</p>";
echo "<p>変更後のセッション名: " . session_name() . "</p>";
?> 
</body>
</html>    

session_name.php

全ての PHP スクリプトでセッション名を変更

全ての PHP スクリプトでセッション名を変更するには、php.ini を編集するか .htaccess を利用します。

//php.ini
session.name = MYSESSION
//.htaccess
php_value session.name MYSESSION

セッション ID の取得

セッション ID を取得するには、session_id() 関数を使用します。以下が構文です。

string session_id ([ $id ] )

パラメータ

  • $id(string):id が指定された場合、現在のセッション ID を置換します。その際、この関数は session_start() より前にコールされている必要があります。

パラメータを指定しない場合は、現在のセッション ID を返します。

戻り値

session_id() は現在のセッションのセッション ID を返します。 現在のセッションが存在しない (現在のセッション ID が存在しない) 場合は空文字列 ("") を返します。

以下は、セッション ID を表示する例です。セキュリティの面からは,セッション ID を画面に表示させるのは好ましいことではありませんが,説明上表示しています。

<?php
//セッション開始
session_start();
?>
<!doctype html>
<html lang="ja">
<head>
<meta charset="utf-8">
<title>セッション ID の取得</title>
</head>
<body>
<?php
echo "<p>現在のセッション ID: " . session_id() . "</p>";
//vstcc9s2sdd2o6c2jtq44u3pbncrkkhk のような値(ID)が表示されます
?> 
</body>
</html>

※同じセッション ID を使い続けることは、セキュリティ上好ましくないので、session_regenerate_id() 関数を使ってセッション ID を変更するようにします。

セッション変数の破棄

セッション変数を破棄するには、変数の割当を解除する unset() 関数を使用します。以下が構文です。

void unset( $var [, $... ] )

パラメータ

  • $var(mixed):破棄(削除)したい変数

戻り値

値を返しません。

全てのセッション変数を破棄(初期化)したい場合は、以下のように空要素の配列をセッション変数($_SESSION)に代入します。

$_SESSION = array();

【注意】unset($_SESSION) によって全ての $_SESSION を初期化してはいけません。 $_SESSION スーパーグローバル変数を用いたセッション変数の登録(使用)ができなくなってしまいます。

以下は、セッション変数を破棄する例です。

<?php
//セッション開始
session_start();
?>
<!doctype html>
<html lang="ja">
<head>
<meta charset="utf-8">
<title>セッション変数の破棄</title>
</head>
<body>
<?php
$_SESSION['data1'] = "sample 1";
$_SESSION['data2'] = "sample 2";
$_SESSION['data3'] = "sample 3";

echo "<pre>";
print_r(h($_SESSION));
echo "</pre>";

unset($_SESSION['data1']);

echo "<p>\$_SESSION['data1'] を破棄しました。</p>";

echo "<pre>";
print_r(h($_SESSION));
echo "</pre>";

$_SESSION = array();

echo "<p>\$_SESSION(全てのセッション変数)を破棄しました。</p>";

echo "<pre>";
print_r(h($_SESSION));
echo "</pre>";

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

session_unset.php

セッションの破棄

セッションに登録されたデータを全て破棄するには、session_destroy() 関数を使用します。

この関数は、 セッションに関するグローバル変数($_SESSION)を破棄しません。 また、セッションクッキーを破棄しません。 セッション変数の利用を再開するには session_start() を呼び出す必要があります。

ユーザーがログアウトするときのように、セッションを切断するには、 セッション ID の割り当ても解除する必要があります。セッション ID の受け渡しに クッキーが使用されている場合(デフォルト)には、セッションクッキーも 削除されなければなりません。

以下が構文です。

bool session_destroy ( void )

パラメータ

ありません。

戻り値

成功した場合に TRUE を、失敗した場合に FALSE を返します。

以下は、セッションを破棄する例です。session_destroy() 関数の実行と合わせて、セッション変数とセッションクッキーも削除するようにします。

<?php
//セッション開始(初期化)
session_start();

$_SESSION['sample1'] = "PHP";
$_SESSION['sample2'] = "JavaScript";

//後で確認のために表示するので、セッション変数を別の変数に保存
$old_session = $_SESSION;

//セッション変数を全て初期化(解除)
$_SESSION = array();

//セッションクッキーの削除
if(isset($_COOKIE[session_name()])){
  //setcookie() は、他のあらゆる出力よりも前に記述する必要があります
  setcookie(session_name(), '', time()-42000, '/');
}

//最終的に、セッションを破壊する
session_destroy();

//エスケープ処理を行う関数 h() 
function h($str){
  if(is_array($str)){
    //$str が配列の場合、h()関数をそれぞれの要素について呼び出す
    return array_map('h', $str);    
  }else{
    return htmlspecialchars($str, ENT_QUOTES, 'UTF-8');
  }
}
?>
<!doctype html>
<html lang="ja">
<head>
<meta charset="utf-8">
<title>セッションの破棄</title>
</head>
<body>
<?php
echo "<p>破棄前のセッション情報(\$_SESSION)</p>";

echo "<pre>";
print_r(h($old_session));
echo "</pre>";

echo "<p>破棄後のセッション情報(\$_SESSION)</p>";

echo "<pre>";
print_r(h($_SESSION));
echo "</pre>";

?> 
</body>
</html>

session_destroy.php

セッションの有効期限

セッションの有効期限は php.ini の session.gc_maxlifetime で定義され、既定値は 1440 秒 (24分) です。

セッションの有効期限 を不用意に延ばすことは、セキュリティ面のリスクを高めることになるので注意が必要です。

セッションの有効期限に関する設定項目は以下になります。

有効期限の切れたセッションファイルは、PHP のガーベッジコレクション(GC)が行います。

ガーベッジコレクション(GC)は、session_start()が呼ばれた時(誰かがアクセスしてきたタイミング)に一定の確率で起動するようになっています。

この確立を設定するのが、session.gc_probability(デフォルト:1)と
session.gc_divisor(デフォルト:100)で、
session.gc_probability / session.gc_divisor(デフォルト:1/100)が
ガーベッジコレクション(GC)の起動する確立になります。

セッション ID の変更

セッション ID が漏洩すると、他人にセッションを乗っ取られる(セッションハイジャック)可能性があります。セッション ID の定期的な変更はセッションハイジャックに対する有効な対策です。

セッション ID は、session_regenerate_id() 関数を呼び出すことで変更することができます。

session_start() 関数でセッションを開始した後に session_regenerate_id() 関数を呼び出します。session_regenerate_id() 関数は、現在使っているセッションを終了させることなくセッション ID だけを新しい値に変換してくれます。

以下が、session_regenerate_id() 関数の構文です。

bool session_regenerate_id([ $delete_old_session ] )

パラメータ

$delete_old_session(bool):関連付けられた古いセッションを削除するかどうか。デフォルトは FALSE ですが、必ず TRUE を指定するようにします。

戻り値

成功した場合に TRUE を、失敗した場合に FALSE を返します。

以下は、session_regenerate_id() 関数を使ってセッション ID を変更する例です。

セキュリティの面からは,セッション ID を画面に表示させるのは好ましいことではありませんが,確認のためセッション ID の最初の 10文字を表示しています。

セッション ID は session_id() 関数で取得することができます。

ブラウザのリロードボタンをクリックして、ページを再読み込みすると、セッション ID が変更されていることが確認できます。またカウンタは1つずつ増えていきます。

20行目の isset($_COOKIE[session_name()]) は、セッションクッキーが設定されているかどうかを判定しています。session_name() 関数は、現在のセッション名を取得または設定する関数です。

セッションがクッキーを使ってセッション ID を保存している場合、セッション ID はセッションの名前を持つクッキーに保存されています。

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

$old_id = session_id();  //現在のセッションIDを取得
$disp_old_id = substr($old_id, 0, 10);  //先頭の10文字を取得

session_regenerate_id(TRUE);  //セッションIDを変更

$new_id = session_id();  //変更後のセッションIDを取得
$disp_new_id = substr($new_id, 0, 10);
?>
<!doctype html>
<html lang="ja">
<head>
<meta charset="utf-8">
<title>セッション ID を変更する</title>
</head>
<body>
<?php
if (!isset($_COOKIE[session_name()])){
  echo "初回のアクセスです。";
}else{
  echo "前回のセッションID の最初の10文字: ".$disp_old_id."<br>";
  echo "今回のセッションID の最初の10文字: ".$disp_new_id."<br>";
}

if(!isset($_SESSION['count'])){
	$_SESSION['count'] = 1;    //初回のアクセス
}else{
	$_SESSION['count'] ++;    //2回目以降のアクセス
}
echo "<p>アクセス回数: " . $_SESSION['count'] . "</p>";

?> 
</body>
</html>

session_change_id.php

セッションの設定オプション

php.ini の設定によりセッションの動作が変わります。PHP マニュアルにいろいろなオプションが記載されています。

session.hash_function(デフォルト:0 / 推奨:1)
セッション ID を生成するハッシュ関数を指定します。デフォルトの 0 は MD5(128 bits)です。1 は SHA-1(160 bits)で、こちらの方がビット数が多いので、その分だけ安全と言えます。
session.save_path
セッションファイルを保存するディレクトリを指定します。共有サーバーでは、他人がアクセスできないディレクトリを指定するようにします。(XAMPP のデフォルトは、session.save_path="C:\xampp\tmp" )
現在のセッションデータの保存先は、session_save_path() 関数を引数なしで実行しても確認できます。
session.use_cookies(デフォルト:1 / 推奨:1)
Cookie によりセッション管理を行うかどうかを指定します。必ず 1 (有効)に指定します。
session.use_only_cookies(デフォルト:1 / 推奨:1)
session.use_only_cookies は、 このモジュールがクライアント側へのセッション ID の保存に Cookie のみ を使用することを指定します。 この設定を有効にすることにより、セッション ID を URL に埋め込む攻撃を防ぐことができます。URL によるセッション管理には様々な問題があるため、必ず 1 に指定します。( PHP 5.3.0 以降で、デフォルトは 1)
session.use_trans_sid(デフォルト:0 / 推奨:0)
透過的なセッション ID の付加をするかどうかを指定します。この機能はセキュリティ上の問題が大きいため、特別な場合を除き使用しない(0 を指定)ようにします。この設定を有効にしてしまうと、Cookie からセッション ID を取得できなかった場合、自動的に相対パスのあらゆる URL にセッション ID が付加されることになり、セッション ID が漏洩する危険性が高まります。

セッションのセキュリティ対策

URL によるセッション管理の禁止、セッション ID の定期的な変更、セッション固定化攻撃や CSRF 攻撃への対策等を講じる必要があります。

セッションハイジャック

他人のセッションIDがわかれば、そのセッションを乗っ取ることができ、そのような攻撃をセッションハイジャックと言い、非常に危険な攻撃です。

PHP のセッションでは、セッション名(デフォルトは PHPSESSID)とセッション ID(デフォルトはランダムな MD5 値)がわかれば、セッションハイジャックが可能です。

攻撃者がセッション ID を知るには、いろいろな方法があるので以下のような対策を行う必要があります。

送信元(Referer)情報によるセッション ID の漏洩

PHP のセッション ID はクッキーに保存されるか、URL に埋め込まれますが URL に埋め込まれた場合、セッション ID が漏洩する危険性が高まります。そのため以下のように php.ini の設定を指定することで、URL によるセッション管理を禁止して、クッキーのみでセッション管理を行うようにすることができます。(デフォルトの設定)

//php.ini
session.use_cookies=1

また、php.ini の session.use_trans_sid(デフォルトは無効)を有効にしてしまうと、クッキーからセッション ID を取得できなかった場合、自動的に相対パスの URL にセッション ID が付加されてしまうので危険性が高まります。session.use_trans_sid は有効にしないようにします。

//php.ini
session.use_trans_sid=0

クロスサイトスクリプティング(XSS)によりクッキー情報の漏洩

クロスサイトスクリプティング(XSS)が可能なページがあれば、XSS によりクッキー情報が盗まれてセッションハイジャックされる可能性があります。XSS 対策を確実に行うようにします。

セッション ID を保存するセッションクッキーに httponly 属性を指定する方法も有効です。(但し、JavaScript/jQuery でクッキーの操作ができなくなります)。

通信の盗聴によりセッション ID の漏洩

可能であれば、SSL/TSL 通信を利用してセッションクッキーに secure 属性を指定します。

セッション ID の推測

PHP のセッション ID は、デフォルトでは MD5(128ビット)によるハッシュ値なので、簡単には推測することは難しいです。但し、自分でセッション ID を生成する場合は、推測が難しい値を生成する必要があり注意が必要です。

php.ini の設定で、以下のようにすることでセッション ID の値を SHA1(160ビット)のハッシュ値に変更できるので、こちらの方がより安全になります。

//php.ini
session.hash_function=1

PHP 5.3 以降では、サーバーでサポートされていれば、session.hash_function = sha256 や session.hash_function = sha512 の指定もできるようです。

また、セッション名はセッションクッキーのキーとして保存されるので、デフォルトの「PHPSESSID」のままだとセッションクッキーだと特定されてしまいます。php.ini で以下のようにセッション名を変更するのも対策になると思います。

セッション名は英数字のみで構成されている必要があり、少なくとも英字が1文字含まれている必要があります。

//php.ini
session.name=X07MYS3F

セッション固定化攻撃

セッション ID を推測する代わりに、攻撃者がセッション ID を指定する攻撃を「セッション固定化攻撃」と言います。対策は、URL によるセッションの管理を禁止することです。 php.ini で以下のように設定します。

//php.ini
session.use_only_cookies=1

セッション ID の変更

セッションが開始されるページや、ログイン直後、また一定時間が経過するたびに、session_regenerate_id()関数を使ってセッションIDを変更することは、セッションハイジャック全般に対する有効な対策になります。

パラメータは、古いセッションファイルを削除するかどうかの真偽値で、必ず TRUE を指定するようにします。

session_start();  //セッション開始 
session_regenerate_id(TRUE);  //セッションIDを変更 

セッションファイルによるセッション ID の漏洩

セッションファイル名は「sess_l8clhs9sv7a682dos0637qu0g1」のように「sess_」+「セッションID」により構成されています。つまりセッションファイル名がわかれば、セッション ID もわかってしまいます。

セッションファイルを保存するディレクトリは、php.ini で以下のように設定できるので、共有サーバーでは、他人がアクセスできないディレクトリを指定します。

session.save_path="ディレクトリを指定"
CSRF攻撃

何らかの方法でユーザーにリクエストを送信させることにより、正当なユーザーに意図しない処理を実行させる攻撃方法をCSRF攻撃(クロスサイトスクリプティングフォージェリー)と言います。

これでは何のことかよくわからないので、以下は「情報処理推進機構(IPA)リクエスト強要(CSRF)対策」からの引用です。

リクエスト強要(CSRF)

リクエスト強要(CSRF:Cross-site Request Forgery)とは、別のサイトに用意したコンテンツ上の罠のリンクを踏ませること等をきっかけとして、インターネットショッピングの最終決済や退会等Webアプリケーションの重要な処理を呼び出すようユーザを誘導する攻撃である。

ブラウザが正規の Webコンテンツにアクセスした際には毎回、セッションを維持するために所定の Cookie、Basic認証データあるいは Digest認証データがブラウザから Webサーバ宛に送出されるという性質を、この攻撃は悪用する。

ユーザの意図に反することを検証できないWebアプリケーション

この攻撃の対象となるのは、トランザクション投入のきっかけとなったフォーム画面が自サーバから供給(POST)されたものであることを確認していない Webアプリケーションである。言い換えれば、ブラウザから自動的に得られる情報のみに基づいてユーザやセッションを識別しているナイーブな Webアプリケーションである。このような Webアプリケーションは、ユーザの意図に反してリクエストが送出されたことを検証できない。

具体的には次のようなWebアプリケーションが対象となる。

  • Cookieを用いてセッションIDを搬送している Webアプリケーション
  • Basic認証を用いているWebアプリケーション
  • Digest認証を用いているWebアプリケーション
  • そのほかWebクライアントから自動で得られる情報にもとづきユーザやセッションを識別しているWebアプリケーション

リクエスト強要(CSRF)のメカニズム

攻撃対象とは別の Webサーバに罠のリンクが用意される。ユーザがその罠を踏んで、攻撃手段が仕込まれる。

Cookieをはじめとする認証データ、識別データは、HTTPリクエストの対象コンテンツ(「目的地」)が正規のものでありさえすれば、Webアプリケーションが用意したものでない偽のフォームやハイパーリンク(「偽の出発地」)から発せられたものであっても送出される。例えば、Cookieを用いてセッション管理を行っている Webアプリケーションの場合、ブラウザから自動送出される Cookieに WebセッションIDを搭載し、サーバがそのIDを見て個々のブラウザを識別している。

特段の対策がとられていない Webアプリケーションは、HTTPリクエストに伴って送られてきたCookieに正規のセッションIDが入っていさえすれば、そのリクエストが受け入れて、ユーザ本人の意志に反した処理を行ってしまうおそれがある。

特に、ユーザがログイン中に攻撃されると、ログイン後にしか操作しないような重要なトランザクションを送ってしまうことになる。(注: この攻撃の構図は、ユーザがログインしていることを要さない画面のフォームについても成立しうる。)

リクエスト強要(CSRF)対策

リクエスト強要(CSRF)への対策は、ユーザ本人以外の者が捏造(ねつぞう)したコンテンツに基づいて発せられたHTTPリクエストを Webアプリケーションが受け付けないようにすることである。

そのためには、代金決済やコミュニティ脱退等の重要な処理の場面において、秘密の「照合情報」をWebアプリケーションとブラウザの間でやりとりし、第三者が用意した偽のコンテンツから発せられたリクエストを区別できるようにする

具体的には、フォームを表示するプログラムによって他者が推定困難なランダム値を hiddenフィールドとして埋め込み送信し、フォームデータを処理するプログラムによってそのランダム値がフォームデータ内に含まれていることを確認する。

そのランダム値がフォ-ムデータに含まれていない場合、処理を見合わせるようにする。

対策

対策の1つが「トークン方式」で、セッションの開始時やログイン時にトークン(ランダムな文字列)を発行し、そのトークンが一致する場合にのみ処理を実行します。

固定トークンの発行

処理ページの直前のページ(入力ページ等)に以下のようなコードを記述してトークンを発行します。固定トークン方式では、セッション中にトークンの値が変わりません。

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

//変数 $ticket にトークンの値を代入(隠しフィールドに挿入する値)
$ticket = $_SESSION['ticket'];

まずセッション変数 $_SESSION['ticket'] に値がセットされているかを isset() 関数で調べます。セットされてない場合は、セッション変数にトークンを代入します。

uniqid() 関数は、マイクロ秒単位の現在時刻に基づいた一意な文字列を返します。上記の例では、mt_rand() 関数で乱数を取得しています。以下が構文です。

string uniqid([ $prefix [, $more_entropy ]] )

パラメータ

  • $prefix(string):返される文字列の先頭に付加される接頭辞を指定します。
  • $more_entropy(bool):返される文字列の後に別のランダムな文字列を追加するかどうかを真偽値で指定します。TRUE を指定すると、別のランダムな文字列が追加されます。

戻り値

一意な識別子を文字列で返します。

mt_rand() 関数は、乱数値を生成する関数です。オプションのパラメータ()を指定しなければ、0 から mt_getrandmax() の間の擬似乱数値を返します。

sha1() 関数は、パラメータで与えられた文字列の SHA-1 のハッシュ値を返します。uniqid() 関数で生成した文字列をパラメータに指定して、そのハッシュ値を取得してセッション変数に代入しています。

ハッシュ値とは、あるデータをハッシュ関数(MD5、SHA-1 等)を使って計算した値です。ハッシュ値は元のデータの長さに関わらず決まった長さになり、SHA-1 の場合は 160ビットです。sha1() 関数はデフォルトでは、計算したハッシュ値を 40文字の16進数で返します。

トークンの POST

同時にトークンの値($ticket)をフォームの隠しフィールド(input type="hidden")を設定します。

<form action="session_csrf2.php" method="post">
  ・・・
  <!--以下はトークンを POST する隠しフィールド「ticket」-->
  <input type="hidden" name="ticket" value="<?php echo $ticket; ?>"><br>
  <input type="submit" value="確認画面へ" />
</form>

処理ページでのトークンの確認

処理ページでは、POST された隠しフィールドの値が、サーバーに保存されている値と一致するかを調べて、一致した場合のみ処理を実行するようにします。

//固定トークンを確認(CSRF対策)
if(isset($_POST['ticket'], $_SESSION['ticket'])){
  $ticket = $_POST['ticket'];
  if($ticket !== $_SESSION['ticket']){
    die('不正なアクセスです。(トークン不一致)');
  }
}else{
  die('不正なアクセスです。(トークンなし)');
}

$_POST['ticket'] は、フォームから POST された隠しフィールドの値で、$_SESSION['ticket'] はセッション変数で、直前のページ(入力ページ等)から処理ページに POST された場合、これらの変数はセットされているはずです。

そこでまず、isset() 関数でこれらの値がセットされているかを調べ、これらの変数がセットされていない場合は、不正なアクセスと判断して die() で即座に処理を中止します。

これらの変数がセットされていた場合は、それらの値が一致するかを確認し、一致しない場合は不正なアクセスと判断できるので die() で即座に処理を中止します。

正規のページを経由せずに、外部の攻撃ページから POST された場合はトークンがセッションに保存されていません。またトークンの値は外部からはわからないので、これらをチェックすることで CSRF 対策になります。

このような実装を行えば、トークンの値がわからない限り安全です。

または、処理ページの直前のページでパスワード入力を要求して、処理ページではパスワードが正しい場合のみ処理をするという方法もあります。

但し、クロスサイトスクリプティング(XSS)脆弱性があればトークン方式を実装しても、隠しフィールドのトークンの値が盗まれる可能性があり、その場合はトークン方式による CSRF 対策は無効化されてしまう可能性があります。

サンプル

以下は、メールアドレスを登録するサンプルです。「メールアドレスを入力するページ」「メールアドレスを確認して登録するページ」「完了ページ」からなりますが、このサンプルは CSRF 対策が目的なので、登録などの処理は省略してあります。以下が使用するファイルです。

  • session_csrf1.php(入力ページ)
  • session_csrf2.php(確認ページ)
  • session_csrf3.php(完了ページ)
  • session_validation.php(検証やエスケープ処理の関数を記述したファイル)

以下は入力ページです。

16行目:初回アクセス時は、$_SESSION['email'] はセットされていないので「null」で初期化します。これを行わないと、37行目の $email の出力で「Undefined index」という Notice エラーが発生します。

19~22行目:CSRF対策の固定トークンを発行します。

25行目:変数 $ticket にトークンの値を代入しています。この値をフォームの隠しフィールド(39行目)に挿入します。

session_csrf1.php

<?php
//セッション開始
session_start();
//セッションIDを変更(セッションハイジャック対策)
session_regenerate_id(TRUE); 

//検証及びエスケープ処理の関数のファイルの読み込み
require 'session_validation.php';

//POSTされたデータがあれば独自に定義した関数でチェック
if(count($_POST) > 0) {
  $_POST = checkInput($_POST);
}

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

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

//変数 $ticket にトークンの値を代入
$ticket = $_SESSION['ticket'];
?>
<!doctype html>
<html lang="ja">
<head>
<meta charset="utf-8">
<title>CSRF 対策(固定トークン方式)入力画面</title>
</head>
<body>
<p>E-mail アドレスの登録</p>
<form action="session_csrf2.php" method="post">
  <label for="email">E-mail</label>
  <input type="text" name="email" id="email" value="<?php echo h($email); ?>" size="40">
  <!--以下は確認ページへトークンをPOSTする隠しフィールド「ticket」-->
  <input type="hidden" name="ticket" value="<?php echo $ticket; ?>"><br>
  <input type="submit" value="確認・登録画面へ" />
</form>
</body>
</html> 

以下は、確認ページです。

14~21行目:固定トークンを確認しています。

29行目:エラーメッセージを格納する配列を初期化しています。この例ではメールアドレスの登録だけなので、配列にする必要はありませんが、複数の項目を検証する場合を考えて配列にしています。

31~38行目:POSTされたデータの内容を検証しています。そしてエラーがなければ42行目で POSTされたデータをセッション変数 $_SESSION['email'] に保存します。

52行目から:エラーがある場合は、エラーのメッセージを表示し、入力ページへ戻るボタンを表示します。フォームの隠しフィールドにはトークンの値を入れておきます。

エラーがない場合は、確認のためのメールアドレスを表示し、登録画面へのボタンと、入力画面に戻るボタンを表示し、隠しフィールドにはトークンの値を入れておきます。

session_csrf2.php

<?php
//セッション開始
session_start();
//セッションIDを変更(セッションハイジャック対策)
session_regenerate_id(TRUE); 

//検証及びエスケープ処理の関数のファイルの読み込み
require 'session_validation.php';

//POSTされたデータを独自に定義した関数でチェック
$_POST = checkInput($_POST);

//固定トークンを確認(CSRF対策)
if(isset($_POST['ticket'], $_SESSION['ticket'])){
  $ticket = $_POST['ticket'];
  if($ticket !== $_SESSION['ticket']){
    die('不正な入力です。(トークン不一致)');
  }
}else{
  die('不正な入力です。(トークンなし)');
}

//POSTされたデータを変数に格納
$email = isset($_POST['email']) ? $_POST['email'] : NULL;
//POSTされたデータを整形(前後にあるホワイトスペースを削除)
$email = trim($email);

//エラーメッセージを保存する配列の初期化
$error = array();
  
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(count($error) === 0) {
  //エラーがなければPOSTされたデータをセッション変数に保存
  $_SESSION['email'] = $email;
}
?>
<!doctype html>
<html lang="ja">
<head>
<meta charset="utf-8">
<title>CSRF 対策(固定トークン方式)確認画面</title>
</head>
<body>
<?php
if(count($error) > 0){  //エラーがあった場合
  //エラーを表示
  foreach($error as $val) {
    echo '<p style="color:red">'.h($val) .'</p>';
  }
?>

<form action="session_csrf1.php" method="post">
  <!--入力ページへトークンをPOSTする、隠しフィールド「ticket」-->
  <input type="hidden" name="ticket" value="<?php echo $ticket; ?>">
  <p><input type="submit" value="入力画面へ戻る" ></p>
</form>

<?php
}else{  //エラーがない場合
  echo '<p>以下の E-mail アドレスを登録します。</p>';
  echo '<p>' . h($email). '</p>';
?>

<form action="session_csrf3.php" method="post">
  <!--完了ページへトークンをPOSTする、隠しフィールド「ticket」-->
  <input type="hidden" name="ticket" value="<?php echo $ticket; ?>">
  <p><input type="submit" value="登録する" ></p>
</form>

<form action="session_csrf1.php" method="post">
  <!--入力ページへトークンをPOSTする、隠しフィールド「ticket」-->
  <input type="hidden" name="ticket" value="<?php echo $ticket; ?>">
  <p><input type="submit" value="入力画面へ戻る" ></p>
</form>

<?php 
}
?> 
</body>
</html>

以下は、完了ページです。このページでは、本来ならメールアドレスを登録する処理やメールを送信する処理を行い、その結果により表示する内容を変更しますが、このサンプルではそれらの処理は省略しています。

15~22行目:トークンを確認しています。

31~39行目:メールアドレスを登録する処理が成功した場合、セッションを破棄します。

41行目:メールアドレスを登録する処理が失敗した場合の処理を記述します(省略)。

session_csrf3.php

<?php
header("Content-type: text/html; charset=utf-8");
//セッション開始
session_start();
//セッションIDを変更(セッションハイジャック対策)
session_regenerate_id(TRUE); 

//検証及びエスケープ処理の関数のファイルの読み込み
require 'session_validation.php';

//POSTされたデータを独自に定義した関数でチェック
$_POST = checkInput($_POST);

//固定トークンを確認(CSRF対策)
if(isset($_POST['ticket'], $_SESSION['ticket'])){
  $ticket = $_POST['ticket'];
  if($ticket !== $_SESSION['ticket']){
    die('不正な入力です。(トークン不一致)');
  }
}else{
  die('不正な入力です。(トークンなし)');
}

//変数にセッション変数の値を代入
$email = $_SESSION['email'];

//登録やメール送信の処理などを記述(省略)

//登録やメール送信の処理が成功した場合
//登録が成功した場合はセッションを破棄
$_SESSION = array();   //空の配列を代入し、すべてのセッション変数を消去 

//セッションクッキーの削除
if(isset($_COOKIE[session_name()])){
  //setcookie() は、他のあらゆる出力よりも前に記述る必要があります
  setcookie(session_name(), '', time()-42000, '/');
}
//セッションを破棄
session_destroy(); 

//登録やメール送信の処理が失敗した場合(省略)
?>
<!doctype html>
<html lang="ja">
<head>
<meta charset="utf-8">
<title>CSRF 対策(固定トークン方式)完了画面</title>
</head>
<body>
<?php
//登録やメール送信の処理が成功した場合
echo '<p>ありがとうございます。以下のアドレスの登録が完了いたしました。</p>';
echo '<p>'. h($email). '</p>';

//登録やメール送信の処理が失敗した場合(省略)

?> 
<p><a href="session_csrf1.php">入力画面へ戻る</a></p>
</body>
</html>

以下は、検証エスケープ処理の関数を記述したファイルです。

session_validation.php

<?php
//入力値に不正なデータがないかなどをチェックする関数
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);
    }
    if(preg_match('/\0/', $var)){  //NULLバイト攻撃対策
      die('不正な入力です。');
    }
    if(!mb_check_encoding($var, 'UTF-8')){ //文字エンコードのチェック
      die('不正な入力です。');
    }	
    //改行、タブ以外の制御文字のチェック
    if(preg_match('/\A[\r\n\t[:^cntrl:]]*\z/u', $var) == 0){  
      die('不正な入力です。制御文字は使用できません。');
    }	
    return $var;
  }
}

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

コンタクトフォームの作成

以下は、セッションを使ったコンタクトフォームのサンプルです。

contact1.php

サンプルでは、実際にはメールは送信されません。

このコンタクトフォームは「入力ページ → 確認ページ → 完了ページ」の順で遷移します。

ページデザイン

入力ページ

ユーザーが「名前」「e-mail」「件名」「問い合わせ内容」を入力するページです。

スパム防止のために、画像認証機能(Securimage)も付けます。

確認ページ

入力された値を確認・検証して、不備がある場合は、エラーを表示して再度入力フォームを表示します。

入力された値に問題がなければ、入力された値をユーザーに確認してもらい、間違いがなければ送信ボタンをクリックして送信してもらいます。

完了ページ

問い合わせの受付が完了したことを知らせるページで、入力されたデータをメールで送信し、問題がなければ送信完了のメッセージを表示します。

デザインとプログラムの分離

デザイン(HTML)とプログラム(PHP)が混在している場合、プログラムが長くなってくるとそれぞれの内容がわかりづらくなってきてしまいます。そこで HTML ファイルと PHP プログラムを別々のファイルに分けて管理するようにします。

これは、PHP の include 文や require 文を使うことで簡単にできます。デザイン部分(HTML)を別のファイル(テンプレートと呼びます)にして、プログラム(PHP)からその HTML(テンプレート)を読み込むようにします。

ファイル構成

  • contact1.php:入力ページのプログラム部分
  • contact2.php:確認ページのプログラム部分
  • contact3.php:完了ページのプログラム部分
  • contact1_view.php:入力ページのデザイン部分(テンプレート)
  • contact2_view.php:確認ページのデザイン部分(テンプレート)
  • contact3_view.php:完了ページのデザイン部分(テンプレート)
  • functions.php:必要な関数をまとめて記述したファイル(テンプレートエンジン)

テンプレート「contact1_view.php」「contact2_view.php」「contact3_view.php」と「functions.php」は Webブラウザから直接アクセスする必要はないので、可能であれば Webブラウザ(外部)からアクセスできない領域に配置します。

Webブラウザ(外部)からアクセスできない領域に配置できない場合は適当な場所にフォルダを作成して「.htaccess」で全てのアクセスを拒否(deny from all)するように設定します。

以下はこのサンプルでのフォルダ構成です。

ここでは、わかりやすいように「contact」フォルダ内に全てのファイルを配置します。「libs」フォルダ内に「templates」フォルダを作成し、テンプレートファイルを配置します。また、「libs」フォルダには、「.htaccess」「functions.php」を配置します。

フォルダ構成

このサンプルでは、上記のフォルダ構成にしているので、「libs」フォルダは外部からアクセスできないように、以下の内容の「.htaccess」を配置します。

deny from all

ページ遷移

contact1.php にアクセス
初回:データを初期化して、contact1_view.php にデータを渡し入力画面を表示
初回以降:セッション変数よりデータを取得して、contact1_view.php にデータを渡し入力画面を表示

入力画面にユーザーが情報を入力後、「確認画面へ」をクリック
form 要素の action=”contact2.php” により contact2.php へ

contact2.php では固定トークンを確認し POST されたデータをチェック、整形、及び画像認証を確認
エラーがあった場合:データを contact1_view.php に渡して再度入力画面を表示
エラーがない場合:データをセッション変数に代入、さらにデータを contact2_view.php に渡して確認画面を表示

contact2_view.php で確認画面で入力されたデータを表示
入力画面に戻るオプションとメールを送信するオプションを表示
入力画面に戻るオプション:form 要素の action=”contact1.php” により contact1.php へ
メールを送信するオプション:form 要素の action=”contact3.php” により contact3.phpへ

contact3.php
POSTされたデータをチェック、固定トークンを確認し、メール送信で使用する変数にセッション変数の値を代入してメールを送信。
メールの送信の結果によりメッセージを作成し、そのデータを contact3_view.php に渡して完了画面を表示。

テンプレートを表示する関数

以下はプログラムから、テンプレートを表示する関数です。

テンプレートの表示には、include 文を使います。プログラム部分でセッション変数や POST された値などを配列に格納しているので、それらを配列のキーを変数名とする変数に代入して渡すようにしています。また、その際に変数の値を全てエスケープ処理します。

  • 第1パラメータ($_template):テンプレートファイル名
  • 第2パラメータ($data):テンプレートで使うデータ(配列)
function display($_template, $data) {
  foreach($data as $key => $val){
    //テンプレートで使う変数を生成し、値をエスケープ処理
    $$key = h($val);  //可変変数
  }
  
  //変数作成後は、受け取った「$data」は、不要になるので破棄
  unset($data);
  
  //テンプレートを読み込む
  include dirname(__FILE__) . '/templates/'. $_template;
}

foreach 構文を使い、配列「$data」の全ての要素をループ処理します。その際、可変変数を使ってキーの値($key)を変数名としています。h() 関数は、別途定義してあるエスケープ処理の関数で、値をエスケープ処理します。

$$key = h($val);  //可変変数

「$$key」は $key の値が変数として展開されます。例えば、$data['name'] の場合、変数名は $name になります。

またテンプレートを読み込む(表示する) include 文は以下のようになっています。

include dirname(__FILE__) . '/templates/'. $_template;

「__FILE__」は、PHP の“マジック”定数の1つで、実行中のファイルが絶対パスで定義されます。このサンプルの場合、「C:\xampp\htdocs\...\contact\libs\functions.php」のような functions.php への絶対パスになります。

また、dirname() 関数は、親ディレクトリのパスを返すので、dirname(__FILE__) は、「libs」フォルダの絶対パスになり、dirname(__FILE__) . '/templates/'. $_template は、テンプレートファイルへの絶対パスになります。(参考:パス

functions.php

functions.php には、前述のテンプレートを表示する display() 関数の他に、エスケープ処理をする h() 関数、値を検証する checkInput() 関数を記述します。

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

//テンプレートを表示する関数
//第1パラメータ($_template):テンプレートファイル名
//第2パラメータ($data):テンプレートで使うデータ(配列)
function display($_template, $data) {
  //テンプレートで使う変数を生成し、値をエスケープ処理
  foreach($data as $key => $val){
    $$key = h($val);  
  }
  //変数作成後は、受け取った「$data」は、不要になるので破棄
  unset($data);
  //テンプレートを読み込む
  include dirname(__FILE__) . '/templates/'. $_template;
}

//入力値に不正なデータがないかなどを検証する関数
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;
  }
}

画像認証 CAPTCHA

画像認証 CAPTCHA(スパム投稿防止)には Securimage を利用します。

Securimage のサイトからダウンロードして解凍し、適当な場所に「securimage」というフォルダを作成してそこに配置します。このサンプルでは、「contact」フォルダ内に配置します。

画像と表示された画像を再表示するリンクを表示するには、以下のコードをフォームの表示したい位置に記述します。

<p>
  <img id="captcha" src="securimage/securimage_show.php" alt="CAPTCHA Image">
  <a href="#" onclick="document.getElementById('captcha').src = 'securimage/securimage_show.php?' + Math.random(); return false">
    <img src="securimage/images/refresh.png" alt="別の画像を表示">
  </a>
</p>

また、表示された画像のコードを入力する input 要素を適当な位置に記述します。

<input type="text" name="captcha_code" id="captcha_code" size="15" maxlength="6" placeholder="確認キーワード(必須)">

必要であれば securimage フォルダの中にある securimage_show.php を編集することで、大きさや色などのカスタマイズが可能です。例えば、表示される文字を指定するには以下のように securimage_show.php に記述します。

$img->charset = 'ABCDEFGHKLMNPRSTUVWYZacegkpuvwy23456789';

このサンプルでは、securimage_show.php に画像の大きさ、ひずみ具合、表示される線の数を以下のように指定しています。

カスタマイズ方法は、Securimage Customizing Securimage(カスタマイズ)に記載されています。

// setting height and calculating optimal width
$img->image_height = 50;
$img->image_width = (int)($img->image_height * 2.875);

// set how distorted the characters will be
$img->perturbation = 0.4; 

// drawing lines over the image
$img->num_lines = 1; //number of lines on the image

Securimage の読み込みと生成は以下のように記述します。このサンプルでは「contact2.php」に記述します。

//画像認証ライブラリの読み込み(securimage)
include_once 'securimage/securimage.php';
$securimage = new Securimage();

キーワードが正しいかを判定するには、以下のように記述します。このサンプルではキーワードは name 属性が「captcha_code」の input 要素から POST されます。

if ($securimage->check($_POST['captcha_code']) == false) {
  echo '*画像認証の確認キーワードが誤っています。';   
}

入力ページ

contact1.php 入力ページのプログラム

contact1.php は唯一ユーザーが直接コンタクトフォームにアクセスできるページになります。テンプレートの contact1_view.php, contact2_view.php, contact3_view.php は、.htaccess でアクセスできないように設定されているか、外部からアクセスできない場所に保存されています。contact2.php, contact3.php は、contact1.php にアクセスしてトークンを発行されていないと直接アクセスしてもエラーになります。

この処理は、ユーザーの初回アクセス時、フォーム送信後エラーがあって再度表示される時、フォーム送信後エラーがなく確認ページで「入力画面へ戻る」をクリックした時に行われます。

contact1.php の PHP での処理 は以下のようになります。

最初にセッションを使えるように session_start() 関数を呼び出します。session_start()関数は、必ず、Webブラウザへの出力が行われる前に、呼び出す必要があります。

その直後に session_regenerate_id() 関数を呼び出してセッション ID を変更します(セッションハイジャック対策)。第1パラメータには必ず「TRUE」を指定します。

9行目:テンプレートを表示する display() 関数、エスケープ処理をする h() 関数、値を検証する checkInput() 関数が記述されているファイル(functions.php)を読み込みます。

$data はテンプレート(contact1_view.php)に渡す配列です。最初に初期化しておきます。

15~18行目:テンプレート(contact1_view.php)を表示する際、初回アクセス時は入力データがないのでこの初期化をしないとエラーとなってしまいます。フォーム送信後エラーがなく確認ページから戻ってきた場合は、$_SESSION 変数には値が入っています。但し、フォーム送信後エラーがある場合は、$_SESSION 変数にはまだ何も入っていません。

21~24行目:CSRF 対策の固定トークンを生成しています。

27行目:生成された固定トークンの値を $data['ticket'] に代入しています。30行目の display() 関数によって、固定トークンの値は変数 $ticket に代入されて、contact1_view.php に渡され、その後隠しフィールドに挿入します。

30行目: display() 関数は、配列 $data のキーを変数名とする変数を生成し、それらの値をエスケープ処理してからテンプレート contact1_view.php を表示します。

contact1.php

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

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

//テンプレートエンジン functions.php の読み込み
require 'libs/functions.php';   

//テンプレートに渡す変数の初期化
$data = array(); //配列を初期化

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

//CSRF対策の固定トークンを生成
if(!isset($_SESSION['ticket'])){
  //$_SESSION['ticket']がセットされていなければ、トークンを生成して代入
  $_SESSION['ticket'] = sha1(uniqid(mt_rand(), TRUE));
}

//トークンの値を配列に代入してテンプレートに渡す(隠しフィールドに挿入)
$data['ticket'] = $_SESSION['ticket'];

//入力ページのテンプレートにデータを渡して表示
display('contact1_view.php', $data);	

contact1_view 入力ページのテンプレート

contact1_view は、ユーザーがコンタクトフォームにアクセスした際に表示されるページです。

14行目~20行目:ユーザーがフォームを contact2.php に送信してエラーがあった場合、contact2.php の display('contact1_view.php', $data)によりこのページが表示され、その際にエラーメッセージを表示するための部分です。

27行目~56行目:ユーザーに入力してもらうフォームです。

44行目~49行目:画像認証(Securimage)の部分です。

54行目:CSRF 対策用の確認ページへトークンを POST する、隠しフィールドです。

contact1_view.php

<!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" />   
<title>コンタクト</title>
<link rel="stylesheet" href="style.css">
</head>
<body>
<div id="contents">

<h2>コンタクト </h2>

<!--入力値にエラーがあった場合エラーメッセージを表示-->
<div id="errorDispaly">
<?php if(isset($error)): ?>
<?php foreach($error as $val): ?>
<?php echo $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 $name; ?>" size="50" placeholder="お名前(必須)">
</div>
<div class="form-group">  
  <label for="email">e-mail</label>
  <input type="text" name="email" value="<?php echo $email; ?>" size="50" placeholder="メールアドレス(必須)">
</div>
<div class="form-group"> 
  <label for="subject">件名</label>
  <input type="text" name="subject" value="<?php echo $subject; ?>" size="50" placeholder="件名(必須)">
</div>
<div class="form-group">  
  <label for="body">内容</label>
  <textarea name="body" cols="50" rows="8" placeholder="内容(必須:500文字まで)"><?php echo $body; ?></textarea>
</div>
<div class="form-group">
  <label for="captcha_code">確認キーワード</label>
  <p><img id="captcha" src="securimage/securimage_show.php" alt="CAPTCHA Image"><a id="different_img" href="#" onclick="document.getElementById('captcha').src = 'securimage/securimage_show.php?' + Math.random(); return false"><img id="dif_img" src="securimage/images/refresh2.png" alt="別の画像を表示">(別の画像)</a> </p>
  <p>* 表示されているキーワードを半角英数字でご記入ください。</p>   
  <input type="text" name="captcha_code" id="captcha_code" size="15" maxlength="6" placeholder="確認キーワード(必須)">
</div>    
  <p>
  <input type="submit" value="確認画面へ">
  </p>
  <!--確認ページへトークンをPOSTする、隠しフィールド「ticket」-->
  <input type="hidden" name="ticket" value="<?php echo $ticket; ?>">
</form>
</fieldset>
</div><!--end of #formArea-->
</div><!--end of #contents--> 

</body>
</html>

確認ページ

contact2.php 確認ページのプログラム

ユーザーが入力ページでフォームを入力後、「確認画面へ」のボタンをクリックすると以下の処理が行われます。

セッションを利用するために、session_start() でセッションを開始します。

7~8行目:include_once で画像認証ライブラリ(securimage)を読み込み、new で securimage のインスタンスを生成して画像認証が利用できるようにします。

11行目:POST された値に問題がないかを functions.php に記述されている checkInput() で検証します。

13~21行目:固定トークンを確認(CSRF対策)。
isset($_POST['ticke'], $_SESSIONT['ticke']) で両方の値がセットされているかを確認して、されていなければ処理を中止します。セットされている場合は、隠しフィールドから POST されたトークンの値とセッションに保存されているトークンの値を比較して、値が一致しなければ処理を中止します。

24~29行目:POSTされたデータを変数に格納しています。

33~37行目:入力ページから POST で送信されたデータを、trim() で前後にあるホワイトスペースを削除して整形し、再び変数に代入しています。

40行目:この後の値の検証で発生するエラーを格納するための配列を初期化します。

42~77行目:入力された値が空でないか、形式があっているか、文字数が制限範囲内かなどを全ての値に対して検証しています。詳細は「フォームの検証」を参照ください。

75行目:画像認証の画像に表示された文字列と $_POST['captcha_code'] の値が正しいかを検証しています。

80~88行目:値を検証してエラーがあった場合の処理です。配列 $data を初期化して、エラーの内容の配列($error)、POST された値、トークンの値を代入し、 display() で $data を contact1_view.php に渡して入力ページを表示します。contact1_view.php では、渡された $data からエラーを表示し、POST された値をそれぞれのフィールドに表示します。

89~103行目:値を検証してエラーがない場合の処理です。POST された値をセッション変数に代入し、セッション変数を使えるようにします。配列 $data を初期化して、POST された値、トークンの値を代入し、 display() で $data を contact2_view.php に渡して確認ページを表示します。

contact2.php

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

require 'libs/functions.php';   //テンプレートエンジンの読み込み

//画像認証ライブラリの読み込み(securimage)
include_once 'securimage/securimage.php';
$securimage = new Securimage();

//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;
//以下は画像認証用データ
$captcha_code = isset($_POST['captcha_code']) ? $_POST['captcha_code'] : NULL;


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

//エラーメッセージを保存する配列の初期化
$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文字以内でお願いします。';
}

//画像認証のチェック
if(mb_strlen($captcha_code) <> 6){
  $error[] = '*画像認証の確認キーワードは6文字で入力してください。';
}else if ($securimage->check($captcha_code) == false) {
  $error[] = '*画像認証の確認キーワードが誤っています。';
}

//チェックの結果にエラーがあった場合は、テンプレートの表示に必要な入力されたデータとエラーメッセージを配列「$data」に代入し、display()関数でform1_view.phpを表示
if(count($error) >0){    //エラーがあった場合
  $data = array();
  $data['error'] = $error;
  $data['name'] = $name;
  $data['email'] = $email;
  $data['subject'] = $subject;
  $data['body'] = $body;
  $data['ticket'] = $ticket;
  display('contact1_view.php', $data);
}else{    //エラーがなかった場合
  //POSTされたデータをセッション変数に保存
  $_SESSION['name'] = $name;
  $_SESSION['email'] = $email;
  $_SESSION['subject'] = $subject;
  $_SESSION['body'] = $body;
  
  //確認画面を表示
  $data = array();
  $data['name'] = $name;
  $data['email'] = $email;
  $data['subject'] = $subject;
  $data['body'] = $body;
  $data['ticket'] = $ticket;
  display('contact2_view.php', $data);
}

contact2_view.php 確認ページのテンプレート

ユーザーが入力ページでフォームを入力後、「確認画面へ」のボタンをクリックして、contact2.php で値などが検証されて問題がない場合に、このページは表示されます。

contact2.php の display('contact2_view.php', $data) によりこのページが表示され、渡された $name, $email, $subject, $body を使って入力された値を表示して、ユーザーに確認してもらいます。これらの変数の値は、display() 関数ですでにエスケープ処理されているので、出力の際のエスケープ処理は不要です。

「内容($body)」は改行を表示するため、nl2br() 関数を使用します。

27~29行目:入力ページへ戻るためのフォームです。ユーザーが入力した値は、すでにセッション変数に入っているので、それらの値を POST する必要はありません。

32~35行目:入力された値に問題がない場合は contact3.php に POST します。この時、CSRF対策のトークンの値(33行目)を POST する必要があります。ユーザーが入力した値は、すでにセッション変数に入っているので、それらの値を POST する必要はありません。

contact2_view.php

<!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" />
<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 $name; ?></dd>
  <dt>e-mail:</dt>
  <dd><?php echo $email; ?></dd>
  <dt>件名:</dt>
  <dd><?php echo $subject; ?></dd>
  <dt>内容:</dt>
  <dd><?php echo nl2br($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 $ticket; ?>">
  <p><input type="submit" value="送信する" class="btn_submit"></p>
</form>
</fieldset>

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

完了ページ

contact3.php 完了ページのプログラム

ユーザーが確認ページで、「送信する」のボタンをクリックすると form の action 属性に指定されている contact3.php に POST されます。

完了ページでは、CSRF 対策の固定トークンのチェックとメール送信の処理を行います。

最初に session_start() を呼び出して、セッションを開始します。続いて functions.php を読み込み、checkInput($_POST) で POST されたデータをチェックします。

10~17行目:固定トークンを確認(CSRF対策)。
isset($_POST['ticke'], $_SESSIONT['ticke']) で両方の値がセットされているかを確認して、されていなければ処理を中止します。セットされている場合は、隠しフィールドから POST されたトークンの値とセッションに保存されているトークンの値を比較して、値が一致しなければ処理を中止します。(contact2.php と同じ処理)

19~23行目:変数にセッション変数の値(contact2.php でユーザーの入力内容を検証・整形した値)を代入しています。23行目では、問い合わせ本文の前に問い合わせから来たことがわかりやすいように文字を追加しています。また、これらの値は検証及び整形はしていますが、エスケープ処理はしていないので、問い合わせ本文は h() 関数でエスケープ処理してから変数に代入しています。これらの変数を使って、メールを送信します。

28行目~61行目:メール(sendmail)の送信処理です。(後述)

62行目~65行目:contact3_view へ渡す配列 $data を初期化して、メール処理の結果($result)とその結果によるメッセージ($message)を配列に代入します。そしてそれらの値を display() 関数で、contact3_view へ渡します。

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']);
//以下は PHPMailer を使って HTML メールを送信する場合
//$body = 'コンタクトページからの問い合わせ'. "<br>" . 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);
}

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

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

$data = array();
$data['message'] = $message;
$data['result'] = $result;
display('contact3_view.php', $data);    

ローカル環境(XAMPP)でのメールの設定については「XAMPP を使ってメールを送信」を参照ください。

以下は、ローカル環境(XAMPP)で自分のドメインのメールサーバー()を使ってメールを送信する場合の、sendmail.ini の設定例です。

//sendmail.ini 
smtp_server=mail.example.com  //送信サーバーを指定
smtp_port=587  //送信ポートを指定
auth_username=info@example.com  //認証のためのユーザー ID
auth_password=xxxxxxxx  //パスワード   

sendmail を使ったメールの送信については「mb_send_mail()」を参照ください。

41~45行目:mb_send_mail() 関数を使ってメールを送信します。戻り値の $result には、メールの送信が成功した場合は TRUE が、送信に失敗した場合は FALSE が入ります。

52行目~61行目:メールの送信の結果($result)により、contact3_view.php で表示するメッセージを設定します。

54~55行目:メールの送信が成功した場合はセッションを破棄します。まず、セッション変数を消去して、その後 session_destroy() でセッションを破棄します。

60~63行目:配列 $data にメッセージと送信結果を格納して、 display() で contact3_view.php を表示します。

PHPMailer を使う場合

PHPMailer については「PHPMailer の設定」を参照ください。

この例では、メールアカウントのパスワード、送信先、Bcc、ホスト名などは、外部からアクセスできない別のファイル(phpmailvars.php)に定数として記述して require_once で読み込むようにしています。

phpmailvars.php

<?php
//メールサーバー
define('MAIL_HOST', 'localhost');
//PHPMailer を使って送信するための E-mail アカウント
define('MAIL_USER', 'info@example.com');
//パスワード
define('MAIL_PASSWORD', 'xxxxxxxxxxxx');
//送信先
define('SEND_TO', 'info@example.com');
//Bcc アドレス
define('BCC', 'xxxxxx@xxxxxxxxxx.com');
//TCP ポート
define('PORT', 587);   

以下の例では、PHPMailer を public_html の直下に配置した場合の例です。

PHPMailer のメールの処理部分以外は前述の sendmail の例と同じです。

//--------PHPMailer----------

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

//メールアカウント情報(パスワード等)の読み込み
require_once('/home/username/etc/xxxx/PHPmailer/phpmailvars.php');  

//PHPMailer の読み込み
require_once('/home/username/public_html/PHPMailer/PHPMailerAutoload.php'); 

$mail = new PHPMailer();  //PHPMailer のインスタンスを生成

$mail->IsSMTP();   // SMTP を使用
$mail->Host = MAIL_HOST; // SMTP サーバーを指定(phpmailvars.phpで定義)
$mail->SMTPAuth = true;   // SMTP authentication を有効に
$mail->Username = MAIL_USER;  // SMTP ユーザ名(phpmailvars.phpで定義)
$mail->Password = MAIL_PASSWORD;  // SMTP パスワード(phpmailvars.phpで定義)
$mail->Port = PORT;    // TCP ポートを指定(phpmailvars.phpで定義)
$mail->From = $email;   //差出人アドレス
$mail->FromName = mb_encode_mimeheader($name);   //差出人名
$mail->AddAddress(SEND_TO);  //メールの宛先(phpmailvars.phpで定義)
//$mail->AddAddress(SEND_TO, mb_encode_mimeheader("宛先名")); //日本語の宛先名を表示する場合
$mail->AddBcc(BCC);  //Bcc アドレス(phpmailvars.phpで定義) 
$mail->Subject = mb_encode_mimeheader($subject);   //件名
$mail->WordWrap = 70;  //70 文字で改行(好みで)
$mail->IsHTML(true);   // HTML形式のメールに設定

//日本語用の設定
$mail->CharSet = "iso-2022-jp";
$mail->Encoding = "7bit";
$mail->Body  = mb_convert_encoding($body,"JIS","UTF-8");
$mail->AltBody = mb_convert_encoding($body,"JIS","UTF-8");  //テキスト表示の場合の本文 

//メール送信の結果(真偽値)を $result に代入
$result = $mail->Send();

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

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

$data = array();
$data['message'] = $message;
$data['result'] = $result;
display('contact3_view.php', $data);

contact3_view 完了ページのテンプレート

contact3.php でメールの送信処理が完了すると、成功した場合は $result に true が入り、$message には「ありがとうございます。送信完了いたしました。」という文字列が入ります。

失敗した場合は、$result に false が入り、$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" />
<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 '<a href="contact1.php">送信失敗(入力ページへ)</a>';
}
?></p>
<p><?php echo $message; ?></p>
</div><!--end of #message-->
<?php if(!$result): ?>
<p>送信失敗が継続する場合は、一度ブラウザを閉じてからやり直すとうまくいくことがあります。</p>
<?php endif; ?>

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

contact1.php

サンプルでは、実際にはメールは送信されません。

jQuery の検証を追加

jQuery を使った検証を追加する例です。

contact1_view.php の各コントロール(input/textarea 要素)にはバリデーションの条件を示す「validate」「required」「number」「mail」などの class 属性を付与します。

  • validate:検証対象の要素に付与するクラス
  • required:必須項目に付与するクラス
  • max30, max50 :文字の最大数を検証する項目に付与するクラス
  • mail:メールアドレスを検証する項目に付与するクラス
<form action="contact2.php" method="post" role="form">
<div class="form-group">
  <label for="name">お名前</label>
  <input type="text" name="name" value="<?php echo $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 $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 $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 $body; ?></textarea>
</div>
<div class="form-group">
  <label for="captcha_code">確認キーワード</label>
  <p><img id="captcha" src="securimage/securimage_show.php" alt="CAPTCHA Image"><a id="different_img" href="#" onclick="document.getElementById('captcha').src = 'securimage/securimage_show.php?' + Math.random(); return false"><img id="dif_img" src="securimage/images/refresh2.png" alt="別の画像を表示">(別の画像)</a></p>
  <p><span>* 表示されているキーワードを<strong>半角英数字</strong>でご記入ください。<br>大文字小文字はどちらでも(全て小文字で)大丈夫です。</span></p>   
  <input type="text" name="captcha_code" id="captcha_code" size="15" maxlength="6" placeholder="確認キーワード(必須)" class="validate required exact6">
</div>  
  <p>
  <input class="btn_submit" type="submit" value="確認画面へ">
  </p>
  <!--確認ページへトークンをPOSTする、隠しフィールド「ticket」-->
  <input type="hidden" name="ticket" value="<?php echo $ticket; ?>">
</form> 

CSS ではコントロールなどの基本スタイルを定義するとともに、以下のようなエラー(入力漏れやメールアドレスの入力ミスなど)用のスタイルを記述しておきます。

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

jQuery の検証は以下のようになります。検証用のスクリプトは、 submit イベント内に記述します。

jQuery の検証については「フォームのバリデーション(検証)」を参照ください。

<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>")
  }
  
  $("form").submit(function(){  
    //エラー表示の初期化
    $("p.error").remove();
    $("div").removeClass("error");
    var text = "";
    
    //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");
      return false;
    }
  }) //ここまでが submit イベントを使った検証
  
  //テキストエリアに入力された文字数を表示し、500文字を超えると色を変えて表示
  $("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>

以下は jQuery の検証を追加した contact1_view.php の全文です。

contact1_view.php jQuery 検証追加

<!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" />
<title>コンタクト</title>
<link rel="stylesheet" href="style.css">
</head>
<body>
<div id="contents">

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

<div id="formArea">
<fieldset id="contactForm">
<legend class="contact_form"><span class="form_name">お問い合わせフォーム</span></legend>
<form action="contact2.php" method="post" role="form">
<div class="form-group">
  <label for="name">お名前</label>
  <input type="text" name="name" value="<?php echo $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 $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 $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 $body; ?></textarea>
</div>
<div class="form-group">
  <label for="captcha_code">確認キーワード</label>
  <p><img id="captcha" src="securimage/securimage_show.php" alt="CAPTCHA Image"><a id="different_img" href="#" onclick="document.getElementById('captcha').src = 'securimage/securimage_show.php?' + Math.random(); return false"><img id="dif_img" src="securimage/images/refresh2.png" alt="別の画像を表示">(別の画像)</a></p>
  <p><span>* 表示されているキーワードを<strong>半角英数字</strong>でご記入ください。<br>大文字小文字はどちらでも(全て小文字で)大丈夫です。</span></p>   
  <input type="text" name="captcha_code" id="captcha_code" size="15" maxlength="6" placeholder="確認キーワード(必須)" class="validate required exact6a">
</div>  
  <p>
  <input class="btn_submit" type="submit" value="確認画面へ">
  </p>
  <!--確認ページへトークンをPOSTする、隠しフィールド「ticket」-->
  <input type="hidden" name="ticket" value="<?php echo $ticket; ?>">
</form>
</fieldset>
</div><!--end of #formArea-->
</div><!--end of #contents--> 
<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>")
  }
  
  $("form").submit(function(){  
    //エラー表示の初期化
    $("p.error").remove();
    $("div").removeClass("error");
    var text = "";
    
    //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");
      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>

contact1.php jQuery 検証追加

サンプルでは、実際にはメールは送信されません。

reCAPTCHA を使ってみる

Google の キャプチャ「reCAPTCHA」を使ってみます。

reCAPTCHA を利用するには、Google のユーザーアカウントと Web サイトのドメイン(URL)が必要です。Google にログイン後「Getting Started」ページの左側の「Create an API Key」のリンクをクリックして表示されるページで登録することができます。

reCAPTCHA 登録

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

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

reCAPTCHA Site key

  • Site Key:HTML などで使用するサイトのキーです。
  • Secret Key:PHP の処理で使用する秘密のキーです。(公開してはいけません)

Secret Key を別ファイルに保存

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

recaptchavars.php

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

reCAPTCHA のダウンロード

PHP で検証するために必要な reCAPTCHA のファイルをダウンロードします。

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

reCAPTCHA ダウンロード

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

reCAPTCHA srcフォルダ

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

contact1.php

9行目:require_once で「recaptchavars.php」(Site Key と Secret Key の値)を読み込みます。

32行目:Site Key の値を変数に格納して reCAPTCHA の値をテンプレートに渡します。

contact1.php

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

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

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

//テンプレートエンジンの読み込み
require 'libs/functions.php';   

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

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

//トークンをテンプレートに渡す
$data['ticket'] = $_SESSION['ticket'];

//reCAPTCHA の値(サイトキー)をテンプレートに渡す
$data['siteKey'] = RC_SITE_KEY;

//テンプレートの表示
display('contact1_view.php', $data);	
ウィジェットの表示

簡単にウィジェットを表示するには、以下のように 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 要素を使って表示するようにします。

以下では3つのコールバック関数を設定しています。

  1. ページ読み込み時のコールバック関数(onloadCallback)
  2. ユーザーがウィジェットにチェックをいれた場合に呼び出されるコールバック関数(verifyRecaptcha)
  3. 期限が過ぎた場合のコールバック関数(expiredCallback)
<head>
・・・中略・・・
<script type="text/javascript">
  var onloadCallback = function() {  //読み込み時のコールバック関数の指定 (1)
    grecaptcha.render('recaptcha', {  //div 要素の id 属性
      'sitekey' : "<?php echo $siteKey; ?>",  //サイトキー
      '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行目:サイトキーの値を格納した変数 $siteKey を使ってサイトキーを指定しています。

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 $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>

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 期限切れ

期限が過ぎてチェックが外れたら「チェックボックスをもう一度オンにしてください。」と表示するようにしています(ウィジェット内にもその旨表示されるので、不要かもしれません)。但し、コールバック関数(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 $siteKey; ?>">
</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 パラメータを指定します。

フォーム内にウィジェットを表示するため、Securimage の代わりに以下を記述します。

<div class="form-group">
  <div id="recaptcha" class="g-recaptcha" data-sitekey="<?php echo $siteKey; ?>"></div>
  <script type="text/javascript" src="https://www.google.com/recaptcha/api.js?onload=onloadCallback&render=explicit" async defer></script>
  <noscript>
    <p class="error">JavaSrcipt を有効にしてください。JavaSrcipt が無効の場合、このフォームは機能しません</p>
  </noscript>
</div>

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

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

contact1_view.php

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

<!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" />
<title>コンタクト</title>
<link rel="stylesheet" href="style.css">
<script type="text/javascript">
  var onloadCallback = function() {
    grecaptcha.render('recaptcha', {
      'sitekey' : "<?php echo $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>
</head>
<body>
<div id="contents">

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

<div id="formArea">
<fieldset id="contactForm">
<legend class="contact_form"><span class="form_name">お問い合わせフォーム</span></legend>
<form action="contact2.php" method="post" role="form">
<div class="form-group">
  <label for="name">お名前</label>
  <input type="text" name="name" value="<?php echo $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 $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 $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 $body; ?></textarea>
</div>
<div class="form-group">
  <div id="recaptcha" class="g-recaptcha" data-sitekey="<?php echo $siteKey; ?>"></div>
  <script type="text/javascript" src="https://www.google.com/recaptcha/api.js?onload=onloadCallback&render=explicit" async defer></script>
  <noscript>
    <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 $ticket; ?>">
</form>
</fieldset>
</div><!--end of #formArea-->
</div><!--end of #contents--> 
<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>")
  }
  
  $("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");
      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>

参考ページ:Displaying the widget

contact2.php / 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($secret);  //インスタンスを生成
$resp = $recaptcha->verify($gRecaptchaResponse, $remoteIp);
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 ;
  }
}

以下は、contact2.php の全文です。その他のファイル(contact2_view.php, contact3.php, contact2_view.php)は変更はありません。

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

require 'libs/functions.php';   //テンプレートエンジンの読み込み

//reCAPTCHA の情報の読み込み
require_once('/home/user/etc/xxxxx/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)
$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 ;
  }
}

//チェックの結果にエラーがあった場合は、テンプレートの表示に必要な入力されたデータとエラーメッセージを配列「$data」に代入し、display()関数でform1_view.phpを表示
if(count($error) >0){    //エラーがあった場合
  $data = array();
  $data['error'] = $error;
  $data['name'] = $name;
  $data['email'] = $email;
  $data['subject'] = $subject;
  $data['body'] = $body;
  $data['ticket'] = $ticket;
  display('contact1_view.php', $data);
}else{    //エラーがなかった場合
  //POSTされたデータをセッション変数に保存
  $_SESSION['name'] = $name;
  $_SESSION['email'] = $email;
  $_SESSION['subject'] = $subject;
  $_SESSION['body'] = $body;
  
  //確認画面を表示
  $data = array();
  $data['name'] = $name;
  $data['email'] = $email;
  $data['subject'] = $subject;
  $data['body'] = $body;
  $data['ticket'] = $ticket;
  display('contact2_view.php', $data);
}

contact1.php reCAPTCHA

サンプルでは、実際にはメールは送信されません。