PHPセキュリティ対策:PHPでログイン処理

(1/1)
PHPを使ってログイン処理をプログラムを作る。
ID・パスワードの新規登録、ログイン、ログアウト、ログイン状態の管理といった基本的な機能を用意する。ID・パスワードはデータベースに登録するが、定石通り、パスワードはハッシュ関数によって暗号化して保存する。また、セッション変数とデータベースの両方にログイン状態を記録することで、一定時間後の自動ログアウトや多重ログイン禁止を実現する。
今回のサンプル・プログラムは、「PHPセキュリティ対策:パスワードとcrypt関数」の応用として、データベースとして SQLite を利用し、PHP バージョン5.3 以上を使い、ID・パスワードの新規登録、ログイン/ログアウトの状態遷移を制御する。

(2022年6月27日)FastCGIで正常動作しない不具合を修正
(2022年6月4日)大幅改訂,PHP8対応

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

PHPでログイン処理

目次

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

圧縮ファイルの内容
plogin.phpサンプル・プログラム
pahooInputData.phpデータ入力に関わる関数群。
使い方は「数値入力とバリデーション」「文字入力とバリデーション」などを参照。include_path が通ったディレクトリに配置すること。
plogin.php 更新履歴
バージョン 更新日 内容
2.01 2022/06/27 FastCGIで正常動作しない不具合を修正
2.0 2022/06/04 大幅改訂,PHP8対応
1.0 2018/10/18
pahooInputData.php 更新履歴
バージョン 更新日 内容
1.1 2022/06/04 getValidString() 修正
1.0 2022/05/18

準備

  34: //表示幅(ピクセル)
  35: define('WIDTH', 600);
  36: 
  37: //プログラム・タイトル
  38: define('TITLE_SIGNUP',  '新規登録');
  39: define('TITLE_LOGIN',   'ログイン画面');
  40: define('TITLE_RESULT',  '処理結果');
  41: define('TITLE_LIST',    '登録情報一覧');
  42: 
  43: //バリデーション用
  44: define('ID_MIN', 4);        //IDの最小長
  45: define('ID_MAX', 20);       //IDの最大長
  46: define('ID_PATTERN', "/^[0-9|a-z|A-Z]+$/");     //IDとして許容するパターン
  47: define('PSW_MIN', 8);       //パスワードの最小長
  48: define('PSW_MAX', 20);      //パスワードの最大長
  49: define('PSW_PATTERN', "/^[0-9|a-z|A-Z|\"\#\$\%\&\'\(\)\*\+\,\-\.\/\:\;\<\=\>\?\@\[\\\\^\_\`\{\|\}\~]+$/");      //パスワードとして許容するパターン
  50: 
  51: //セッション関連
  52: define('SESSION_KEY',  'login_pahoo');      //セッション・キー
  53: define('SESSION_LIFE', 2);                  //有効期間(分)
  54: 
  55: //パスワードのハッシュ化
  56: define('DEF_SALT', 'qcq4vp5b9iu5nkcbfkqaic');   //Salt(PHP5.4以下で使用)
  57: if (!defined('PASSWORD_BCRYPT'))    define('PASSWORD_BCRYPT', 0);
  58: 
  59: //SQLite DBファイル名:各自の環境に合わせて変更すること
  60: define('DBFILE', './userinfo.sqlite3');
  61: 
  62: //SQLite テーブル名
  63: define('TABLE_USER', 'user');       //ユーザー情報
  64: 
  65: //実行するSQL
  66: define('PRE_SELECT', 'SELECT * FROM ' . TABLE_USER . ' WHERE id=:id');
  67: define('PRE_INSERT', 'INSERT INTO ' . TABLE_USER . ' (id, psw, session, dt, premiere, latest) VALUES (:id, :psw, :session, :dt, :premiere, :latest)');
  68: define('PRE_SESSION', 'UPDATE ' . TABLE_USER . ' SET session=:session, dt=:dt WHERE id=:id');
  69: define('PRE_LIST', 'SELECT * FROM ' . TABLE_USER . ' WHERE 1');
  70: 
  71: //データ入力に関わる関数群:include_pathに配置すること
  72: require_once('pahooInputData.php');

上記の定数は任意に変更できる。
後述するが、入力されたIDやパスワードの長さ、文字種を厳格にチェックするため、バリデーション用の定数を細かく用意している。
たとえば、IDは半角英数字(大文字・小文字識別)、パスワードは半角英数記号(大文字・小文字識別)としたが、ID_PATTERNPSW_PATTERN を変更することで、文字種の制約を変更できる。許容パターンは正規表現である。

ログイン状態はセッションで管理しており、有効期間は SESSION_LIFE に定義する。

ID、パスワードを保存するデータベースは、インストールせずに利用できる SQLite を採用した。データベースファイルは DBFILE によって任意の場所に作成できる。

データ入力やパスワード処理に関わる関数群は別ファイル "pahooInputData.php" に分離しており、include_path が通ったディレクトリに配置すること。

画面遷移

プログラムは1本のPHPファイルだが、セッションの有無、POST で渡される mode の値によって、機能別の画面を表示できるようにしてある。画面遷移は下図の通りである。
PHPでログイン処理

登録情報一覧

ログイン画面の「登録情報一覧」リンクを押下すると、データベースに登録されたID・パスワードが下図のように一覧表示される。通常のログイン機能では必要のない画面だが、パスワードがハッシュ化されていることを明らかにするための参照機能として用意した。
PHPでログイン処理

ログイン状態の管理

ログイン状態は、セッション変数とデータベースの両方を使って管理する。
今回は、次の条件でログイン状態かどうかを判断する。
  1. セッション変数 $_SESSION[SESSION_KEY] にIDがあるかどうか
  2. データベースのセッションIDと一致するかどうか
  3. データベースの最終アクセス日時と現在時刻の差が SESSION_LIFE 以内かどうか
データベースを併用することによって、多重ログインを禁止できる。ここでは、先にログインした方が SESSION_LIFE の期間中、ログイン状態を独占できるようにした(先勝ち方式)。意図的にログアウトするか、SESSION_LIFE の期間中に画面移動を行わなければ、ログイン状態は解除される。

 446: /**
 447:  * ログイン状態かどうか
 448:  * @param  string $errmsg エラーメッセージ格納用(無ければ空文字)
 449:  * @return    bool TRUE:ログイン状態/FALSE:ではない
 450: */
 451: function islogged(&$errmsg) {
 452:     $res = FALSE;
 453:     if (isset($_SESSION[SESSION_KEY]) && ($_SESSION[SESSION_KEY!'')) {
 454:         if (getLoggedStatus($_SESSION[SESSION_KEY], $errmsg) == 1) {
 455:             $res = TRUE;        //ログイン中
 456:         }
 457:     }
 458:     return $res;
 459: }

 810: //セッション開始
 811: session_set_cookie_params(SESSION_LIFE * 60);
 812: session_start();
 813: 
 814: //DB初期化
 815: if (DBinit() == FALSE) {
 816:     $title  = TITLE_RESULT;
 817:     $errmsg = 'データベースの初期化に失敗しました';
 818:     $html = makeHTML_header($title. makeHTML_result('', $errmsg. makeHTML_footer();
 819:     echo $html;
 820:     exit(1);
 821: }
 822: 
 823: //動作モード
 824: $mode = getParam('mode', FALSE, '');
 825: if ($mode == 'logout')      logout($errmsg);        //ログアウト
 826: if (islogged($errmsg))      $mode = 'logged';       //ログイン中

ユーザー関数 islogged は、上記の条件に基づき、ログイン状態を取得する。
メインプログラムの冒頭に上述のように記載することで、ログイン状態かどうかの判定を行うことができる。

 371: /**
 372:  * データベースからログイン状態を取得
 373:  * @param  string $id     ID
 374:  * @param  string $errmsg エラーメッセージ格納用(無ければ空文字)
 375:  * @return    int  0:セッション破棄
 376:  *             1:セッション維持
 377:  *             2:二重ログイン
 378:  *             3:エラー
 379: */
 380: function getLoggedStatus($id, &$errmsg) {
 381:     $errmsg = '';
 382:     if ($res = DBget_session($id, $session, $dt, $errmsg)) {
 383:         $dt1 = new DateTime('now');
 384:         $dt2 = new DateTime($dt);
 385:         $t2 = dateInterval2Seconds($dt1->diff($dt2, TRUE));
 386:         if ($t2 > (SESSION_LIFE * 60)) {
 387:             DBset_session($id, '', '', $errmsg);
 388:             unset($_SESSION[SESSION_KEY]);
 389:             $res = 0;
 390:         } else if (($session !''&& ($session !session_id())) {
 391:             $errmsg = $id . ' はすでにログインしています';
 392:             $res = 2;
 393:         } else if (isset($_SESSION[SESSION_KEY])) {
 394:             $session = session_id();
 395:             $dt      = date(DATE_W3C, time());
 396:             $res = DBset_session($id, $session, $dt, $errmsg);
 397:             $res = $res ? 1 : 3;
 398:         } else {
 399:             $res = 0;
 400:         }
 401:     } else {
 402:         $res = 3;
 403:     }
 404:     return $res;
 405: }

ユーザー関数 islogged が呼び出すユーザー関数 getLoggedStatus は、ログイン状態を管理する。

まず、ユーザー関数 DBget_session を呼び出し、ログイン時にデータベースに記録したセッションID $session と、最後に DBget_session を呼び出した日時 $dt を取得する。
$dt と現在日時の差分が SESSION_LIFE より大きければ、データベースのセッションIDと日時をクリアする(ログイン状態ではない)。これにより、ログインしたままという状態を回避する。

日時差分が SESSION_LIFE より小さく、セッションIDがデータベースに登録してあるものと異なれば、多重ログインと判断する。

最後に、セッション変数が存在していればログイン状態を維持していると判断し、データベースの最終アクセス日時を現在日時に更新する。

ログイン/ログアウト

 407: /**
 408:  * ログインする
 409:  * @param  string $id     ID
 410:  * @param  string $errmsg エラーメッセージ格納用(無ければ空文字)
 411:  * @return    bool TRUE:ログイン成功/FALSE:失敗
 412: */
 413: function login($id, &$errmsg) {
 414:     $res = getLoggedStatus($id, $errmsg);
 415:     if ($res  <2) {
 416:         $session = session_id();
 417:         $dt = date(DATE_W3C, time());
 418:         $res = DBset_session($id, $session, $dt, $errmsg);
 419:         if ($res == TRUE)   $_SESSION[SESSION_KEY] = $id;
 420:     } else {
 421:         $res = FALSE;
 422:     }
 423:     return $res;
 424: }

 426: /**
 427:  * ログアウトする
 428:  * @param  string $errmsg エラーメッセージ格納用(無ければ空文字)
 429:  * @return    bool TRUE:登録成功/FALSE:失敗
 430: */
 431: function logout(&$errmsg) {
 432:     $errmsg = '';
 433:     $res = TRUE;
 434:     if (isset($_SESSION[SESSION_KEY])) {
 435:         $id = $_SESSION[SESSION_KEY];
 436:         if ($id !'') {
 437:             $res = DBset_session($id, '', '', $errmsg);
 438:             if ($res == TRUE)   unset($_SESSION[SESSION_KEY]);
 439:         } else {
 440:             $res = FALSE;
 441:         }
 442:     }
 443:     return $res;
 444: }

ログイン処理を行う関数 login は、まず、前述のユーザー関数 getLoggedStatus を呼び出し、多重ログインになっていないかを検査し、ログイン処理を行う。つまり、データベースのセッションIDと最終アクセス日時を更新し、セッション変数 $_SESSION[SESSION_KEY] にIDを代入する。

ログアウト処理を行う関数 logout は、ログインの逆で、データベースのセッションIDと最終アクセス日時をクリアし、セッション変数を削除する。

データベース処理

 133: /**
 134:  * DBの初期化
 135:  * @param  なし
 136:  * @return    bool TRUE/FALSE
 137: */
 138: function DBinit() {
 139:     try {
 140:         $pdo = new PDO('sqlite:' . DBFILE);
 141:         $pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
 142: 
 143:         //テーブル作成:ユーザー情報
 144:         //id:ユーザーID, psw:パスワード名, 
 145:         //premiere:登録日時, latest:更新日時
 146:         $pdo->exec('CREATE TABLE IF NOT EXISTS ' . TABLE_USER . '(
 147:         id           TEXT PRIMARY KEY,
 148:             psw          TEXT,
 149:             session      TEXT,
 150:             dt           TEXT,
 151:             premiere     TEXT,
 152:             latest       TEXT
 153:         )');
 154:     $res = TRUE;
 155:     } catch (PDOException $e) {
 156:         $res = FALSE;
 157:     }
 158: 
 159:     return $res;
 160: }

冒頭で紹介したとおり、データベースは SQLite を採用した。
メインプログラムの冒頭で、ユーザー関数 DBinit を呼び出し、データベースファイルが存在しなければ自動的に CREATE するようにしている。
ID、パスワードを検索するSELECTや、新規登録のためのINSERT処理については、「PHPとデータベース」を参考にしてほしい。

参考サイト

(この項おわり)
header