Web開発に携わったことがある方であれば「セッション」という用語を一度は聞いたことがあるかと思います。
特にWebシステムを構築する場合は、セッション管理の問題を避けては通れません。
そんなセッション管理ですが、「ページ遷移時にセッションが消えてしまう」といった事象で困ったことはありますでしょうか。
今回の記事では、そういった問題に対する注意点をまとめながら、PHPを使ってWebサイトのセッションを管理する方法を紹介していきます。
目次
PHP・SESSIONの使い方 基礎知識
PHPによるセッション管理方法を学ぶ前に「セッション」とはどういうものか、簡単にまとめておきましょう。
セッションとは
セッションとは「Webサイトへのアクセス開始から離脱するまでの一連の流れ」を1セッションとして扱う「アクセス解析の単位」のことです。
基本的には「あるユーザーがWebサイトにアクセスしてからブラウザを閉じる(別のページに移動する)」までを1セッションとしてカウントします。
なお、1セッションで1ページしか閲覧しない場合もあれば、1セッションで複数のページを閲覧する場合もあります。
例えば、「トップページ」「会社概要」「採用案内」という3つのページで構成されたホームページに、あるユーザーがトップページにアクセスし、ページ内のリンクを経由して会社概要、採用案内を閲覧した場合でも1セッションとしてカウントされますが、別のユーザーがトップページだけアクセスしてすぐにブラウザを閉じた場合でも1セッションとしてカウントされます。
ただし、Webページを開いたままにし、ページ閲覧とページ閲覧の間に一定時間が経過した場合には新しいセッションとしてカウントされてしまうことがあります。
トップページにアクセスし、しばらく何も操作せず30分経過した後で会社概要を閲覧したような場合では、新たなセッションがカウントされ、2セッションとしてカウントされることになるでしょう。
セッションには、こういった特性があるため「同じ利用者が一連の流れでWebサイトにアクセスしていること」を判定するために使用されています。
「Webシステムへのログイン状態の保持」といった機能や「Googleアナリティクス」などのような大規模なアクセス解析などが代表的な使われ方となります。
セッションで管理するもの
通常、Webデータのやりとりには「HTTPプロトコル」と呼ばれるものが使われており、1回のアクセス要求に対する応答が完了したら必ず通信を切断しています。
このような仕組みで「ユーザー認証」が必要な会員制のページにアクセスした場合、ページを移動するたびに通信が切断されることになるため、Webサーバー側では「認証を受けたユーザーかどうか」を判定することができません。
そこで「同じ利用者が一連の流れでWebサイトにアクセスしていること」を管理する「セッション管理」が必要となってきます。
「セッション管理」をすることで、アクセスしてきたユーザーに対し1つの「セッションID」を発行し「同一のユーザーであること」や「認証されたユーザーであること」を判定できるようになります。
クッキーとセッション
Webサイトにアクセスし、Webサーバーから返ってくる応答の中に「クッキーを作成してください」という指示があった時に作成されるのが「クッキー」です。
「クッキー」には「名前:値」の情報や「有効期限」「クッキーを送信する対象ドメイン」などが保存されることになります。
「セッション管理」を行う際に発行される「セッションID」は、この「クッキー」に保存されることになり、Webサーバーとクライアント間でクッキーに保存されたセッションIDをやりとりすることで「同一ユーザー」であるかどうかを判定します。
PHP・SESSIONの使い方 実装方法
「セッション管理」についての基本知識のおさらいはこのくらいにして、実際にPHPを使ってセッションを管理する方法の説明に移りたいと思います。
セッションの開始
PHPでのセッションの使い方はいたってシンプルで、以下のコードをPHPファイルに記述することでセッションが管理できるようになります。
1 2 3 |
<?php session_start(); ?> |
「session_start()」が実行されることでサーバー側にセッションIDのファイルが生成されます。
セッションID
セッションIDについては「Google chrome」などのブラウザを使って確認することができます。
「Google chrome」でセッションIDを確認する方法は以下の通りです。
- Google chromeのツールバーにある「表示」→「開発/管理」→「デベロッパーツール」を表示
- 「Cookies」を展開し、アクセスしているURLを選択
- 「Appplication」タブを選択
PHPでセッション管理をしている場合、「Name」が「PHPSESSID」となっている箇所の「Value」に格納されている値がセッションIDです。
ログイン認証機能でセッション管理をする場合、ユーザーIDとパスワードを照合して認証した後に、あらたなセッションIDを発行し、ログイン前のセッションIDとログイン後のセッションIDを区別することで「ログインしている状態」を保持させることが可能となります。
変数の登録
「同一のユーザーであること」「ログインした状態であること」が確保されているセッション情報であれば、他にも様々な情報を登録して「ユーザーの状態に応じた情報」を保持させることが可能となります。
たとえばログイン後の画面でユーザーの「名前」を表示させたり、「ある処理を実行した後かどうか」といった情報を保持させたい場合は「セッション変数」を使用することになります。
セッション変数を指定する場合は以下のように記述します。
1 |
$_SESSION[セッション変数名] = 値; |
なお、このようなセッション変数を使って「ユーザーの状態に応じた情報」を管理することは、Webサービスを構築する上で非常に重要な処理となります。
特にショッピングサイトのような商品購入の処理を行うWebサービスでは、ユーザーの状態が「注文前」なのか「注文後」なのかを管理しておかなければ、画面を「リロード」することで商品購入の処理が二重に実行されてしまい、思わぬトラブルとなってしまう危険があります。
こういったことを防ぐために「セッション変数」が用意されていると言っても過言ではありませんので、しっかりと把握しておくとよいでしょう。
セッションの削除
ログアウトの機能を構築する場合、セッション情報をクリアする必要があります。
登録したセッション変数を削除する場合は以下のように記述します。
1 |
$_SESSION = array(); |
また、セッション自体をクリアする場合は以下のように記述します。
1 |
@session_destroy(); |
セッションの有効期限
セッション情報には有効期限があり、一定時間、ページ遷移などの操作を行わなかった場合にはあらたなセッションIDが割り当てられてしまい、格納したセッション変数などが利用できなくなってしまいます。
PHPでのセッションの有効期限は「php.ini」ファイルで指定することができ、何も指定しなければ1440秒(24分)が設定されています。
このように自動でセッションの情報が消去されてしまっていることがあるため、実際にWebサービスを構築する場合は、セッションが消えてしまった場合のことを考えて処理を構築しておかなければなりません。
PHP・SESSIONの使い方 二重処理の防止
それでは早速、セッションを実装させる練習としてサンプルプログラムを作成してみましょう。
サンプルプログラムは、注文フォームから商品と数量を選択して「購入」ボタンをクリックすることで注文受付の画面が表示される、といった機能を実装します。
Order.phpの作成
まずは「注文フォーム」の画面を作成します。
テキストエディタで「Order.php」ファイルを作成し、以下のソースコードを記述してWebサーバにアップしてください。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 |
<?php session_start(); $commo = [ '1' =>['銅の剣',100], '2' =>['鋼の剣',1200], '3' =>['炎の剣',2300] ]; ?> <!doctype html> <html> <head> <meta charset="UTF-8"> <title>注文</title> </head> <body> <h1>注文フォーム</h1> <form action="Ordered.php" method="POST"> <p>商品:<select name="commodity" style="font-size:1.2em;width:180px"> <?php foreach($commo as $key=>$vals){ echo '<p><option value='.$key.'>'.$vals[0].':'.$vals[1].'G</option></p>'; } ?> </select> 数量:<select name="qua" style="font-size:1.2em;width:60px"> <?php for($i=1;$i<5;$i++){ echo '<option value='.$i.'>'.$i.'</option>'; } ?> </select></p> <input type="submit" value="商品を購入する"> </form> </body> </html> |
Ordered.phpの作成
次に「注文受付」の画面を作成します。
同様に「Ordered.php」を作成し、以下のソースコードを記述してください。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 |
<?php session_start(); $db['host'] = "◯◯◯◯"; // DBサーバのURL $db['user'] = "◯◯◯◯"; // ユーザー名 $db['pass'] = "◯◯◯◯"; // ユーザー名のパスワード $db['dbname'] = "◯◯◯◯"; // データベース名 $commo = [ '1' =>['銅の剣',100], '2' =>['鋼の剣',1200], '3' =>['炎の剣',2300] ]; try { $dsn = sprintf('mysql:host=%s; dbname=%s; charset=utf8', $db['host'], $db['dbname']); $pdo = new PDO($dsn, $db['user'], $db['pass'], array(PDO::ATTR_ERRMODE=>PDO::ERRMODE_EXCEPTION)); if (!empty($_POST["commodity"]) && !empty($_POST["qua"])) { $post_commo = $_POST['commodity']; $post_qua = $_POST['qua']; $stmt = $pdo->prepare("INSERT INTO t_order(t_order_commo,t_order_qua,t_order_date) VALUES (:t_order_commo,:t_order_qua,now())"); $stmt->execute(array(':t_order_commo' => $post_commo,':t_order_qua' => $post_qua)); $_SESSION["OrderState"] = '注文後'; } $sql ="SELECT * FROM t_order"; $stmt =$pdo->query($sql); $orderlog=$stmt->fetchAll(); $orderlog_id = array_column($orderlog,'t_order_id'); $orderlog_commo = array_column($orderlog,'t_order_commo'); $orderlog_qua = array_column($orderlog,'t_order_qua'); $orderlog_date = array_column($orderlog,'t_order_date'); } catch (PDOException $e) { echo 'データベースエラー'; } ?> <!doctype html> <html> <head> <meta charset="UTF-8"> <title>注文受付</title> </head> <body> <h1>注文受付</h1> <div>- - - - - - - - - - - - - - - - - - - - </div> <?php if(count($orderlog_id) > 0){ for($i=0;$i<count($orderlog_id);$i++){ echo '注文番号:'.$orderlog_id[$i].'<br>'; echo '商品:'.$commo[$orderlog_commo[$i]][0].'<br>'; echo '数量:'.$orderlog_qua[$i].'個<br>'; echo '金額:'.$commo[$orderlog_commo[$i]][1].'G × '.$orderlog_qua[$i].'個 '.$commo[$orderlog_commo[$i]][1]*$orderlog_qua[$i].'G'; echo '<div>- - - - - - - - - - - - - - - - - - - - </div>'; } }else{ echo '注文履歴はありません' ; } ?> <ul> <li><a href="Order.php">注文画面にもどる</a></li> </ul> </body> </html> |
二重処理防止にはセッションとトークンを使う
このプログラムでは、「ユーザーが注文した後かどうか」を判定していないため、注文受付画面を「リロード」することで、何度も同じ注文がどんどん追加されてしまいます。
「リロード」や「戻る」をクリックした時に二重処理されないようにするためには、以下のようにソースコードを修正してください。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 |
<?php session_start(); $commo = [ '1' =>['銅の剣',100], '2' =>['鋼の剣',1200], '3' =>['炎の剣',2300] ]; $token = uniqid('', true); $_SESSION['token'] = $token; ?> <!doctype html> <html> <head> <meta charset="UTF-8"> <title>注文</title> </head> <body> <h1>注文フォーム</h1> <form action="OrderedR.php" method="POST"> <p>商品:<select name="commodity" style="font-size:1.2em;width:180px"> <?php foreach($commo as $key=>$vals){ echo '<p><option value='.$key.'>'.$vals[0].':'.$vals[1].'G</option></p>'; } ?> </select> 数量:<select name="qua" style="font-size:1.2em;width:60px"> <?php for($i=1;$i<5;$i++){ echo '<option value='.$i.'>'.$i.'</option>'; } ?> </select></p> <input type="hidden" name="token" value="<?php echo $token;?>"> <input type="submit" value="商品を購入する"> </form> </body> </html> |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 |
<?php session_start(); $db['host'] = "◯◯◯◯"; // DBサーバのURL $db['user'] = "◯◯◯◯"; // ユーザー名 $db['pass'] = "◯◯◯◯"; // ユーザー名のパスワード $db['dbname'] = "◯◯◯◯"; // データベース名 $commo = [ '1' =>['銅の剣',100], '2' =>['鋼の剣',1200], '3' =>['炎の剣',2300] ]; if(isset($_POST["token"]) && isset($_SESSION["token"])){ $token = $_POST["token"]; $session_token = $_SESSION["token"]; } unset($_SESSION["token"]); try { $dsn = sprintf('mysql:host=%s; dbname=%s; charset=utf8', $db['host'], $db['dbname']); $pdo = new PDO($dsn, $db['user'], $db['pass'], array(PDO::ATTR_ERRMODE=>PDO::ERRMODE_EXCEPTION)); if (!empty($_POST["commodity"]) && !empty($_POST["qua"])) { if($token != "" && $token == $session_token) { $post_commo = $_POST['commodity']; $post_qua = $_POST['qua']; $stmt = $pdo->prepare("INSERT INTO t_order(t_order_commo,t_order_qua,t_order_date) VALUES (:t_order_commo,:t_order_qua,now())"); $stmt->execute(array(':t_order_commo' => $post_commo,':t_order_qua' => $post_qua)); $_SESSION["OrderState"] = '注文後'; }else{} } $sql ="SELECT * FROM t_order"; $stmt =$pdo->query($sql); $orderlog=$stmt->fetchAll(); $orderlog_id = array_column($orderlog,'t_order_id'); $orderlog_commo = array_column($orderlog,'t_order_commo'); $orderlog_qua = array_column($orderlog,'t_order_qua'); $orderlog_date = array_column($orderlog,'t_order_date'); } catch (PDOException $e) { echo 'データベースエラー'; } ?> <!doctype html> <html> <head> <meta charset="UTF-8"> <title>注文受付</title> </head> <body> <h1>注文受付</h1> <div>- - - - - - - - - - - - - - - - - - - - </div> <?php if(count($orderlog_id) > 0){ for($i=0;$i<count($orderlog_id);$i++){ echo '注文番号:'.$orderlog_id[$i].'<br>'; echo '商品:'.$commo[$orderlog_commo[$i]][0].'<br>'; echo '数量:'.$orderlog_qua[$i].'個<br>'; echo '金額:'.$commo[$orderlog_commo[$i]][1].'G × '.$orderlog_qua[$i].'個 '.$commo[$orderlog_commo[$i]][1]*$orderlog_qua[$i].'G'; echo '<div>- - - - - - - - - - - - - - - - - - - - </div>'; } }else{ echo '注文履歴はありません' ; } ?> <ul> <li><a href="OrderR.php">注文画面にもどる</a></li> </ul> </body> </html> |
セッションとトークンを使って二重処理を防止する仕組みを構築しておりますので、サンプルを見ながら理解を深めていただければと思います。
PHP・SESSIONの使い方 セッション管理の注意点
セッションを管理するにあたり、サーバーの設定による制限に気づかず、意図した通りのセッション管理ができない可能性があります。
そんな時に確認すべき設定や、注意すべき点をいくつかまとめておきましたので、参考にしてください。
php.iniのsession.auto_start
PHPでセッションを扱う場合、「php.ini」の「session.auto_start」の値を変更することでセッションを自動的にスタートさせるかどうかを設定することができます。
「session.auto_start」をON(値を1)にするとユーザーがサイトにアクセスした時に自動的にセッションが開始されます。
この設定をすることで「session_start();」を記述する必要はなくなるのですが、session.auto_startをONにすると、セッション変数に「オブジェクト」を代入することができなくなってしまいますので注意が必要です。
セッション変数にオブジェクトを代入する処理が動作していない、といった場合は、session.auto_startがONになっていないかどうかを確認するとよいでしょう、
session_start()の位置
セッション自体がうまく開始できない、といった時の原因として「PHPファイルの先頭に<?php session_start() ?>が記述されていないこと」というミスが多いようです。
詳細な説明は省きますが、<?php session_start() ?>はレスポンスヘッダに出力される前に記述しなければ意図したとおりのセッション管理ができません。
<?php session_start() ?>を記述する前に改行しただけでも、最初のセッション開始がうまく動作しないこともあるため注意が必要です。
1 |
Warning: Cannot modify header information - headers already sent by (・・・ |
といったメッセージが表示されてしまう場合は、<?php session_start() ?>が記述されている位置を確認するとよいでしょう。
session_save_pathへのアクセス権
セッションIDのファイルを格納する場所は「php.ini」ファイル内の「session_save_path」で指定されており、デフォルト以外のパスに変更することもできるのですが、対象パスのアクセス権限に問題がある場合にエラーとなってしまうため、注意が必要です。
session_save_pathで指定されているフォルダのアクセス権に「書き込み」の権限が付与されているかどうか確認するとよいでしょう。
PHP・SESSIONの使い方 ログイン機能への実装
それでは最後に、実際にセッション管理をつかったログイン認証機能を実装させてみましょう。
以前の記事で紹介したレンタルサーバーの「XFree」上にPHP+MySQLの動作環境が構築されていることが前提となりますので、構築がまだの方は以前の記事を参考に環境を構築してください。
Login.phpの作成
まずはログイン画面を作成します。
テキストエディタで「Login.php」を作成し、以下のコードを記述してWebサーバーにアップしてください。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 |
<?php session_start(); $db['dbname'] = "◯◯◯◯"; // データベース名 $db['user'] = "◯◯◯◯"; // ユーザー名 $db['pass'] = "◯◯◯◯"; // ユーザー名のパスワード $db['host'] = "◯◯◯◯"; // DBサーバのURL //エラーメッセージの初期化 $errorMessage = ""; //ログインボタンが押した時の処理 if (isset($_POST["login"])) { //入力チェック if (empty($_POST["userid"])) { $errorMessage = 'ユーザーIDが未入力です。'; } else if (empty($_POST["password"])) { $errorMessage = 'パスワードが未入力です。'; } if (!empty($_POST["userid"]) && !empty($_POST["password"])) { $userid = $_POST["userid"]; $password = $_POST["password"]; $dsn = sprintf('mysql:host=%s; dbname=%s; charset=utf8', $db['host'], $db['dbname']); try { $pdo = new PDO($dsn, $db['user'], $db['pass'], array(PDO::ATTR_ERRMODE=>PDO::ERRMODE_EXCEPTION)); $stmt = $pdo->prepare('SELECT * FROM t_user WHERE t_user_id = ?'); $stmt->execute(array($userid)); if ($row = $stmt->fetch(PDO::FETCH_ASSOC)) { if($password == $row['t_user_password']){ session_regenerate_id(true); // 入力したIDのユーザー名を取得 $id = $row['t_user_id']; $sql = "SELECT * FROM t_user WHERE t_user_id = $id"; //入力したIDからユーザー名を取得 $stmt = $pdo->query($sql); foreach ($stmt as $row) { $row['t_user_name']; } $_SESSION["NAME"] = $row['t_user_name']; header("Location: Main.php"); exit(); } else { $errorMessage = 'ログインできませんでした'; } } else { $errorMessage = 'ログインできませんでした'; } } catch (PDOException $e) { $errorMessage = 'データベースエラー'; } } } ?> <!doctype html> <html> <head> <meta charset="UTF-8"> <title>ログイン</title> </head> <body> <h1>ログイン画面</h1> <form id="loginForm" name="loginForm" action="" method="POST"> <p>ログインフォーム</p> <div><font color="#ff0000"><?php echo htmlspecialchars($errorMessage, ENT_QUOTES); ?></font></div> <label for="userid">ユーザーID</label> <input type="text" id="userid" name="userid" placeholder="ユーザーIDを入力" value="<?php if (!empty($_POST["userid"])) {echo htmlspecialchars($_POST["userid"], ENT_QUOTES);} ?>"> <br> <label for="password">パスワード</label><input type="password" id="password" name="password" value="" placeholder="パスワードを入力"> <br> <input type="submit" id="login" name="login" value="ログイン"> </form> </body> </html> <!-- ※1 このプログラムは学習用のためパスワードを暗号化していません。 セキュリティの強度が必要な場合は、このプログラムを使わないでください。 --> |
Main.phpの作成
続いてメイン画面を作成します。
同様に「Main.php」を作成し、以下のコードを記述してください。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
<?php session_start(); // ログイン状態チェック if (!isset($_SESSION["NAME"])) { header("Location: Logout.php"); exit; } ?> <!doctype html> <html> <head> <meta charset="UTF-8"> <title>メイン</title> </head> <body> <h1>メイン画面</h1> <p>ようこそ<u><?php echo htmlspecialchars($_SESSION["NAME"], ENT_QUOTES); ?></u>さん</p> <ul> <li><a href="Logout.php">ログアウト</a></li> </ul> </body> </html> |
Logout.phpの作成
最後にログアウト画面を作成します。
こちらも同様に「Logout.php」を作成してWebサーバーにアップしてください。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 |
<?php session_start(); if (isset($_SESSION["NAME"])) { $errorMessage = "ログアウトしました。"; } else { $errorMessage = "セッションがタイムアウトしました。"; } // セッションの変数のクリア $_SESSION = array(); // セッションクリア @session_destroy(); ?> <!doctype html> <html> <head> <meta charset="UTF-8"> <title>ログアウト</title> </head> <body> <h1>ログアウト画面</h1> <div><?php echo htmlspecialchars($errorMessage, ENT_QUOTES); ?></div> <ul> <li><a href="Login.php">ログイン画面に戻る</a></li> </ul> </body> </html> |
headerでのページ遷移
Login.phpの45行目で「Main.php」に遷移する処理を行っていますが、この処理はユーザーIDとパスワードがデータベースに登録されているユーザー情報と合致し、「ユーザーが認証された状態」で実施されることになります。
そこでのセッションに関連する処理は、36行目の「session_regenerate_id(true);」で新たなセッションIDを発行し、44行目の「$_SESSION[“NAME”] = $row[‘t_user_name’];」でデータベースから取得した「名前(t_user_name)」をセッション変数「NAME」に格納しています。
Main.phpに遷移した後は、あらたに発行されたセッションIDで「同一のユーザーかどうか」を判定し、セッション変数に格納された「名前」がブラウザ上に出力されることになります。
まとめ
いかがでしたか。
セッションの管理方法が把握できましたでしょうか。
サンプルプログラムのソースを参考にしていただければ、Webシステムに二重の処理を防ぐためにセッション管理が重要であることがお分かりいただけるかと思います。
なお、今回掲載したサンプルプログラムではパスワードのハッシュ化の部分が考慮されておりませんので、別の記事にてハッシュ関数を使ってパスワードをハッシュ化する方法を紹介させていただければと考えております。
今回は「PHPでのセッション管理」についての説明となりますので、今後、PHPでのセッション管理を実装しなければならない時がありましたら、こちらの記事を思い出していただければ幸いです。