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

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

(2025年11月9日)ID_PATTERN, PSW_PATTERNの指定ミスを訂正
(2025年11月2日)パスワードは 👁ボタンを押している間だけ表示
(2025年8月15日)2要素認証(Google Autheticator)に対応,大幅改訂

目次

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

圧縮ファイルの内容
plogin.phpサンプル・プログラム
pahooInputData.phpデータ入力に関わる関数群。
使い方は「数値入力とバリデーション」「文字入力とバリデーション」などを参照。include_path が通ったディレクトリに配置すること。
pahooQRimage.phpQRコード(2次元バーコード)作成に関わる暮らす。
使い方は「PHPでQRコードをつくる」などを参照。include_path が通ったディレクトリに配置すること。
plogin.php 更新履歴
バージョン 更新日 内容
3.1.1 2025/11/09 ID_PATTERN, PSW_PATTERNの指定ミスを訂正
3.1.0 2025/11/02 パスワードは 👁ボタンを押している間だけ表示
3.0.0 2025/08/15 2要素認証機能追加, 大幅改訂
2.01 2022/06/27 FastCGIで正常動作しない不具合を修正
2.0 2022/06/04 大幅改訂,PHP8対応
pahooInputData.php 更新履歴
バージョン 更新日 内容
2.0.1 2025/08/11 getParam() bug-fix
2.0.0 2025/08/11 pahooLoadEnv() 追加
1.9.0 2025/07/26 getParam() 引数に$trim追加
1.8.1 2025/03/15 validRegexPattern() debug
1.8.0 2024/11/12 validRegexPattern() 追加
pahooQRimage.php 更新履歴
バージョン 更新日 内容
1.0.1 2025/07/29 text2QRimage() -- bug-fix
1.0.0 2025/07/29 初版

サインアップの流れ

サインアップの流れを下図に示す。
プログラムは1本のPHPファイルだが、セッションの有無、POST で渡される mode の値によって、機能別の画面を表示できるようにしてある。
サインアップの流れ
ログイン画面
ログイン画面
本プログラムのメイン画面はログイン画面である。
「サインアップ」をクリックすると、サインアップ処理に入る
サインアップ画面
サインアップ画面
新しいアカウントのID、パスワードを入力する。それぞれの文字種、文字数の制限は画面に表示されているとおり。これらは後述する定数によって自由に変更できる。
2要素認証するかどうか
2要素認証するかどうか
2要素認証は必須ではない。この画面で「いいえ」を選ぶと、ID、パスワードのみでログインできる。「はい」を選ぶと、2要素認証を登録する画面に遷移する。
2要素認証を登録
2要素認証を登録
2要素認証は、シークレットコードをスマホアプリ「Google Authenticator」(Android版iOS版)に登録する。
スマホアプリ「Google Authenticator」を使って、画面に表示している QRコード(2次元バーコード)を読むことで、シークレットキー(セットアップキー)をスマホアプリに登録する。
以後、ログインする都度、Google Authenticator が表示する6桁の数字(コード)を入力する流れになる。

ログインの流れ

ログインの流れを下図に示す。
ログインの流れ
ログイン画面
ログイン画面
本プログラムのメイン画面はログイン画面である。
ID、パスワードを入力し、「ログイン」ボタンをクリックする。
ログイン成功画面
ログイン成功画面
2要素認証を設定していないアカウントの場合、ログイン成功画面を表示する。
実際のアプリケーションでは、この画面を雛形として処理を進める。ログイン状態はセッション SESSION_KEY に保管されている。
認証コード入力画面
認証コード入力画面
2要素認証を設定しているアカウントの場合、認証コード入力画面を表示する。
Google Authenticator を使い、該当するアカウントの認証コード(数字6桁)を入力する。
なお、プログラムでは、ログイン画面でID、パスワードの照合を行っているので、仮ログイン状態(セッション SUBSESSION_KEY を発行)しておき、認証コードが合致したら、あらためてセッション SESSION_KEY を発行する。

登録情報一覧

ログイン画面の「登録情報一覧」リンクを押下すると、データベースに登録されたID、パスワード、シークレットキー(2要素認証を登録していない場合は空)や最終ログイン日時を下図のように一覧表示する。パスワードとシークレットキーは暗号化されており、たとえシステム管理者と言えども知ることができないようになっている。
通常のログイン機能では必要のない画面だが、パスワードがハッシュ化されていることを明らかにするための参照機能として用意した。

余談になるが、アカウントの認証に用いるパスワードやシークレットキーは、企業トップやシステム管理者が把握してはいけないと考えている。それを可能にすれば、なりすましができてしまうからだ。何かトラブルが起きたとき、運用ルールで制約を欠けているだけでは不十分で、確実になりすましができないことを証明する意味で、このような仕様にした。
もしアカウント利用者がパスワードやシークレットキーを忘れてしまったら、面倒でも再発行(再サイインン)させるべきだ。
登録情報一覧
登録情報一覧

準備:pahooInputData 関数群

PHPのバージョンや入力データのバリデーションなど、汎用的に使う関数群を収めたファイル "pahooInputData.php" が同梱されているが、include_path が通ったディレクトリに配置してほしい。他のプログラムでも "pahooInputData.php" を利用するが、常に最新のファイルを1つ配置すればよい。

また、各種クラウドサービスに登録したときに取得するアカウント情報、アプリケーションパスワードなどを登録した .pahooEnv ファイルから読み込む関数 pahooLoadEnv を備えている。こちらについては、「各種クラウド連携サービス(WebAPI)の登録方法」をご覧いただきたい。

準備:pahooQRimage クラス

pahooQRimage.php

  41: class pahooQRimage {
  42: public $dataPath;               // データファイルのあるパス
  43: public $imagePath;              // QRコードのイメージフレームのあるパス
  44: public $maxQRversion = 40;      // 最大バージョン
  45: public $maxImageSize = 1480;    // QRコードの最大サイズ(一辺のピクセル)
  46: 
  47: public $qrcode_data_string;
  48: public $qrcode_module_size;
  49: public $qrcode_image_type;
  50: 
  51: public $qrcode_structureappend_n;
  52: public $qrcode_structureappend_m;
  53: public $qrcode_structureappend_parity;
  54: public $qrcode_structureappend_originaldata;
  55: 
  56: function __construct() {
  57:     $this->dataPath = __DIR__ . '/QRdata';
  58:     $this->imagePath = __DIR__ . '/QRimage';
  59: 
  60:     $this->qrcode_structureappend_n = '';
  61:     $this->qrcode_structureappend_m = '';
  62:     $this->qrcode_structureappend_parity = '';
  63:     $this->qrcode_structureappend_originaldata = '';
  64: }

QRコード(2次元バーコード)を生成するためのメソッドを集めたのが pahooQRimageクラス である。同梱のクラス・ファイル "pahooQRimage.php" は include_path が通ったディレクトリに配置してほしい。内容については「PHPでQRコードをつくる」をご覧いただきたい。他のプログラムでも pahooQRimageクラス を利用することがあるが、常に最新のクラス・ファイルを1つ配置すればよい。

なお、QRコードを生成するのに、"QRimage" フォルダに格納している画像ファイル群と、"QRdata" フォルダに格納しているデータ・ファイル群が必要になる。クラス・ファイル "pahooQRimage.php" と同じディレクトリに配置してほしい。

準備:各種定数

plogin.php

  58: // 各種定数(START) =========================================================
  59: 
  60: // 表示幅(ピクセル)
  61: define('WIDTH', 600);
  62: 
  63: // アプリ名(Google Authenticatorに表示する名前)
  64: define('APPNAME',  'pahooデモアプリ');
  65: 
  66: // 画面タイトル
  67: define('TITLE_SIGNUP',      'サインアップ');
  68: define('TITLE_LOGIN',       'ログイン');
  69: define('TITLE_CODE',        '認証コード入力');
  70: define('TITLE_RESULT',      '登録結果');
  71: define('TITLE_STATUS',      'ログイン状態');
  72: define('TITLE_selsected',   '2要素認証するかどうか');
  73: define('TITLE_ADDSECRET',   '2要素認証登録');
  74: define('TITLE_LIST',        'アカウント一覧');
  75: define('TITLE_ERROR',       '処理エラー');
  76: 
  77: // バリデーション用
  78: define('ID_MIN', 4);        // IDの最小長
  79: define('ID_MAX', 50);       // IDの最大長
  80: define('ID_PATTERN', "/^[0-9|a-z|A-Z|\"\#\$\%\&\'\(\)\*\+\,\-\.\/\:\;\<\=\>\?\@\[\\\\^\_\`\{\|\}\~]+$/");       // IDとして許容するパターン(メールアドレス対応)
  81: define('PSW_MIN', 8);       // パスワードの最小長
  82: define('PSW_MAX', 20);      // パスワードの最大長
  83: define('PSW_PATTERN', "/^[0-9a-z\!\#\$\%\&\'\*\/\=\?\^\_\+\-\`\{\\}\~\.]+$/");  // パスワードとして許容するパターン
  84: define('CODE_MIN', 6);      // 認証コードの最小長【変更不可】
  85: define('CODE_MAX', 6);      // 認証コードの最大長【変更不可】
  86: define('CODE_PATTERN', "/^[0-9]+$/");   // 認証コードとして許容するパターン
  87: 
  88: // セッション関連
  89: define('SESSION_KEY',  'login_pahoo');              // セッション・キー
  90: define('SESSION_LIFE', 5);                          // 有効期間(分)
  91: define('SUBSESSION_KEY', 'secondVerify_pahoo');     // サブセッション・キー
  92: 
  93: // パスワードのハッシュ化
  94: define('DEF_SALT', 'qcq4vp5b9iu5nkcbfkqaic');   // Salt(PHP5.4以下で使用)
  95: if (! defined('PASSWORD_BCRYPT'))   define('PASSWORD_BCRYPT', 0);
  96: 
  97: // SQLite DBファイル名:各自の環境に合わせて変更すること
  98: define('DBFILE', './userinfo.sqlite3');
  99: 
 100: // SQLite テーブル名
 101: define('TABLE_USER', 'user');
 102: 
 103: // 各種定数(END) ===========================================================

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

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

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

解説:Google認証クラス

plogin.php

 141: class pahooGoogleAuthenticator {
 142: 
 143: // 認証コード長
 144: protected $_codeLength = 6;
 145: 
 146: /**
 147:  * シークレット・キーを生成する
 148:  * @param   int $secretLength シークレット・キーの長さ(省略時:16)
 149:  * @return  string シークレット・キー
 150: */
 151: function createSecret($secretLength = 16) {
 152:     $validChars = $this->_getBase32LookupTable();
 153:     unset($validChars[32]);
 154: 
 155:     $secret = '';
 156:     for ($i = 0$i < $secretLength$i++) {
 157:         $secret .$validChars[array_rand($validChars)];
 158:     }
 159:     return $secret;
 160: }
 161: 
 162: /**
 163:  * 認証コードを生成する
 164:  * @param   string $secret シークレット・キー
 165:  * @param   int    $timeSlice UNIXタイムスタンプを30秒ごとに区切った通し番号(省略時:現時点の通し番号)
 166:  * @return  string 認証コード
 167: */
 168: function getCode($secret, $timeSlice = NULL) {
 169:     // UNIXタイムスタンプ通し番号
 170:     if ($timeSlice === NULL) {
 171:         $timeSlice = floor(time() / 30);
 172:     }
 173: 
 174:     $secretkey = $this->_base32Decode($secret);
 175: 
 176:     $time = chr(0).chr(0).chr(0).chr(0).pack('N*', $timeSlice);
 177:     $hm = hash_hmac('sha1', $time, $secretkey, true);
 178:     $offset = ord(substr($hm, -1)) & 0x0F;
 179:     $hashpart = substr($hm, $offset, 4);
 180: 
 181:     $value = unpack("N", $hashpart);
 182:     $value = $value[1& 0x7FFFFFFF;
 183: 
 184:     $modulo = pow(10, $this->_codeLength);
 185:     return str_pad($value % $modulo, $this->_codeLength, '0', STR_PAD_LEFT);
 186: }
 187: 
 188: /**
 189:  * 認証コードを照合する
 190:  * @param   string $secret シークレット・キー
 191:  * @param   string $code   照合したい認証コード
 192:  * @param   int    $currentTimeSlice UNIXタイムスタンプを30秒ごとに区切った通し番号(省略時:現時点の通し番号)
 193:  * @param   string $discrepancy 照合範囲(省略時:1‥‥前後1回の認証コードも一致とする)
 194:  * @return  bool TRUE:一致, FALSE:不一致
 195: */
 196: function verifyCode($secret, $code, $discrepancy = 1, $currentTimeSlice = NULL) {
 197:     // UNIXタイムスタンプ通し番号
 198:     if ($currentTimeSlice === NULL) {
 199:         $currentTimeSlice = floor(time() / 30);
 200:     }
 201: 
 202:     // 認証コードのバリデーション
 203:     if (strlen($code!6) {
 204:         return FALSE;
 205:     }
 206: 
 207:     // 認証コードを照合する
 208:     for ($i = -$discrepancy$i <$discrepancy$i++) {
 209:         $calculatedCode = $this->getCode($secret, $currentTimeSlice + $i);
 210:         if (hash_equals($calculatedCode, $code)) {
 211:             return TRUE;
 212:         }
 213:     }
 214:     return FALSE;
 215: }
 216: 
 217: /**
 218:  * QRコード用otpauth形式テキストを取得する
 219:  * @param   string $name   アプリケーション名
 220:  * @param   string $secret シークレット・キー
 221:  * @param   string $title  イシュア名
 222:  * @param   string $code   照合したい認証コード
 223:  * @param   array  $params 追加パラメータ
 224:  * @return  string otpauth形式テキスト
 225: */
 226: function getQRCodeGoogleUrl($name, $secret, $title = NULL, $params = []) {
 227:     $urlencoded = urlencode("otpauth://totp/{$name}?secret={$secret}");
 228:     if (isset($title)) {
 229:         $urlencoded .urlencode("&issuer={$title}");
 230:     }
 231:     foreach ($params as $key => $value) {
 232:         $urlencoded .urlencode("&{$key}={$value}");
 233:     }
 234: 
 235:     return $urlencoded;
 236: }
 237: 
 238: /**
 239:  * 認証コード長を設定する
 240:  * @param   int $length 認証コード長
 241:  * @return  なし
 242: */
 243: function setCodeLength($length) {
 244:     $this->_codeLength = $length;
 245: }
 246: 
 247: /**
 248:  * シークレット・キーをbase32デコードする
 249:  * @param   string $secret シークレット・キー
 250:  * @return  string デコードした文字列
 251: */
 252: function _base32Decode($secret) {
 253:     if (empty($secret)) return '';
 254: 
 255:     $base32chars = $this->_getBase32LookupTable();
 256:     $base32charsFlipped = array_flip($base32chars);
 257: 
 258:     $paddingCharCount = substr_count($secret, '=');
 259:     $allowedValues = [6, 4, 3, 1, 0];
 260:     if (!in_array($paddingCharCount, $allowedValues)) return FALSE;
 261: 
 262:     for ($i = 0$i < 4$i++) {
 263:         if ($paddingCharCount == $allowedValues[$i&&
 264:             substr($secret, -($allowedValues[$i])) !str_repeat('=', $allowedValues[$i]))  return FALSE;
 265:     }
 266: 
 267:     $secret = str_replace('=', '', $secret);
 268:     $secret = str_split($secret);
 269:     $binaryString = '';
 270:     for ($i = 0$i < count($secret); $i = $i + 8) {
 271:         $x = '';
 272:         if (!in_array($secret[$i], $base32chars)) return FALSE;
 273: 
 274:         for ($j = 0$j < 8$j++) {
 275:             $x .str_pad(base_convert(@$base32charsFlipped[@$secret[$i + $j]], 10, 2), 5, '0', STR_PAD_LEFT);
 276:         }
 277: 
 278:         $eightBits = str_split($x, 8);
 279:         for ($z = 0$z < count($eightBits); $z++) {
 280:             $binaryString .= (chr(base_convert($eightBits[$z], 2, 10)));
 281:         }
 282:     }
 283: 
 284:     return $binaryString;
 285: }
 286: 
 287: /**
 288:  * base32デコード用ルックアップテーブルを取得する
 289:  * @param   なし
 290:  * @return  array ルックアップテーブル
 291: */
 292: function _getBase32LookupTable() {
 293:     return [
 294:         'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H',
 295:         'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P',
 296:         'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X',
 297:         'Y', 'Z', '2', '3', '4', '5', '6', '7',
 298:         '='
 299:     ];
 300: }
 301: 
 302: }
 303: // End of Class ===========================================================

2要素認証に関わるメソッドは、Google認証クラス pahooGoogleAuthenticator として分離した。
シークレットキー(セットアップキー)の生成、認証コードの生成と照合などを行う。

Google Authenticator では、シークレットキーを otpauth形式テキストとしてQRコード化する。その書式は下記の通り。
otpauth://totp/(アプリ名称)?secret=(シークレットキー)

解説:データの暗号化処理

plogin.php

 318: /**
 319:  * データを暗号化する.
 320:  * 暗号化/復号化のキーにMYSELFを使用しているので,
 321:  * プログラム名を変更するとデータベースに登録済みのデータが利用できなくなる.
 322:  * @param   string $data 暗号化したいデータ
 323:  * @return  string 暗号化データ
 324: */
 325: function pahooEncrypt($data) {
 326:     $cipher = 'aes-256-cbc';
 327:     $key = hash('sha256', MYSELF, TRUE);
 328:     $ivLength = openssl_cipher_iv_length($cipher);
 329:     $iv = openssl_random_pseudo_bytes($ivLength);   // ランダムIV(16バイト)
 330: 
 331:     // 暗号化
 332:     $encrypted = openssl_encrypt($data, $cipher, $key, OPENSSL_RAW_DATA, $iv);
 333: 
 334:     // 復号のために IV と暗号文を合体して base64エンコード
 335:     return base64_encode($iv . $encrypted);
 336: }

plogin.php

 338: /**
 339:  * 暗号化データを復号する.
 340:  * @param   string $encryptData 暗号データ
 341:  * @return  string 復号されたデータ
 342: */
 343: function pahooDecrypt($encryptData) {
 344:     $cipher = 'aes-256-cbc';
 345:     $key = hash('sha256', MYSELF, TRUE);
 346:     $ivLength = openssl_cipher_iv_length($cipher);
 347: 
 348:     $data = base64_decode($encryptData);
 349:     $ivDec = substr($data, 0, $ivLength);
 350:     $ciphertextDec = substr($data, $ivLength);
 351: 
 352:     return openssl_decrypt($ciphertextDec, $cipher, $key, OPENSSL_RAW_DATA, $ivDec);
 353: }

本プログラムでは、セキュリティのため、パスワードはハッシュ関数を使って暗号化してからデータベースに登録する。
2要素認証に使うシークレットコードも暗号化する必要があるが、シークレットコードは認証コード照合時に必要になるので、ハッシュ関数のように復号化ができない暗号化処理は利用できない。そこで、 openssl_encrypt  を使って復号可能な暗号データとしてデータベースに登録する。 openssl_decrypt  を使うことで復号できる。ここでは、共通鍵暗号方式 AES (Advanced Encryption Standard) を 256ビット鍵長で使い、暗号モードに CBC (Cipher Block Chaining) を採用した AES-256-CBC を用いているが、PHPで利用できる暗号化方式であればなんでもよい。

解説:DBの初期化

plogin.php

 356: /**
 357:  * DBの初期化
 358:  * @param   なし
 359:  * @return  bool TRUE/FALSE
 360: */
 361: function DBinit() {
 362:     try {
 363:         $pdo = new PDO('sqlite:' . DBFILE);
 364:         $pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
 365: 
 366:         // テーブル作成:ユーザー情報
 367:         // id:ユーザーID, psw:パスワード(ハッシュ化),
 368:         // secret:シークレット・キー(暗号化)
 369:         // ※2要素認証しないアカウントについては,secretは空文字
 370:         // session:セッションキー
 371:         // dt:セッション更新日時, lastlogin:最終ログイン日時
 372:         // premiere:登録日時, latest:更新日時
 373:         $pdo->exec('CREATE TABLE IF NOT EXISTS ' . TABLE_USER . '(
 374:          id           TEXT PRIMARY KEY,
 375:             psw          TEXT,
 376:             secret       TEXT,
 377:             session      TEXT,
 378:             dt           TEXT,
 379:             lastlogin    TEXT,
 380:             premiere     TEXT,
 381:             latest       TEXT
 382:         )');
 383:      $res = TRUE;
 384:     } catch (PDOException $e) {
 385:         var_dump($e);
 386:         $res = FALSE;
 387:     }
 388: 
 389:     return $res;
 390: }

アカウントの管理、最終ログイン時の記録などは SQLite で行う。
本プログラム起動時にデータベースファイルが無ければ、DBinit を呼び出す。使用するテーブルは user の1つだけで、その構造はコメントに記載したとおりだ。

解説:ログイン状態の管理

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

plogin.php

 843: /**
 844:  * ログイン状態かどうか
 845:  * @param   string $errmsg エラーメッセージ格納用(無ければ空文字)
 846:  * @return  bool TRUE:ログイン状態/FALSE:ではない
 847: */
 848: function islogged(&$errmsg) {
 849:     $res = FALSE;
 850:     if (isset($_SESSION[SESSION_KEY]) && ($_SESSION[SESSION_KEY!'')) {
 851:         if (getLoggedStatus($_SESSION[SESSION_KEY], $errmsg) == 1) {
 852:             $res = TRUE;        // ログイン中
 853:         }
 854:     }
 855:     return $res;
 856: }

plogin.php

1570: // セッション開始
1571: session_set_cookie_params(SESSION_LIFE * 60);
1572: session_start();
1573: 
1574: // DB初期化
1575: if (DBinit() == FALSE) {
1576:     $title  = TITLE_RESULT;
1577:     $errmsg = 'データベースの初期化に失敗しました';
1578:     $html = makeHTMLheader($title. makeHTMLresult('', $errmsg. makeHTMLfooter();
1579:     echo $html;
1580:     exit(1);
1581: }
1582: 
1583: // ID取得
1584: $errmsg = '';
1585: $idPat = array(ID_PATTERN);
1586: $id = getValidString('id', $errmsg, '', ID_MIN, ID_MAX, TRUE, $idPat);
1587: if ($errmsg !'') {
1588:     $title = TITLE_ERROR;
1589:     $html  = makeHTMLheader($title. makeHTMLresult($title, $errmsg. makeHTMLfooter();
1590:     echo $html;
1591:     exit(1);
1592: }
1593: 
1594: // パスワード取得
1595: $errmsg = '';
1596: $pswPat = array(PSW_PATTERN);
1597: $psw = getValidString('psw', $errmsg, '', PSW_MIN, PSW_MAX, TRUE, $pswPat);
1598: if ($errmsg !== '') {
1599:     $title = TITLE_ERROR;
1600:     $html  = makeHTMLheader($title. makeHTMLresult($title, $errmsg. makeHTMLfooter();
1601:     echo $html;
1602:     exit(1);
1603: }
1604: 
1605: // 認証コード取得
1606: $errmsg = '';
1607: $codePat = array(CODE_PATTERN);
1608: $code = getValidString('code', $errmsg, '', CODE_MIN, CODE_MAX, TRUE, $codePat);
1609: if ($errmsg !== '') {
1610:     $title = TITLE_ERROR;
1611:     $html  = makeHTMLheader($title. makeHTMLresult($title, $errmsg. makeHTMLfooter();
1612:     echo $html;
1613:     exit(1);
1614: }
1615: 
1616: // 動作モード取得
1617: $mode = getParam('mode', FALSE, '');
1618: // ログアウト
1619: if ($mode == 'logout') {
1620:     logout($errmsg);
1621: // 仮ログイン中
1622: else if (issublogged($errmsg)) {
1623:     $mode = 'sublogged';
1624:     $id = $_SESSION[SUBSESSION_KEY];
1625:     if (isButton('login2')) {
1626:         $mode = 'login2';
1627:     }
1628: // ログイン中
1629: else if (islogged($errmsg)) {

ユーザー関数 islogged は、上記の条件に基づき、ログイン状態を取得する。
メインプログラムの冒頭に上述のように記載することで、ログイン状態かどうかの判定を行うことができる。
なお、2要素認証を登録しているアカウントでは、ログインに成功してから認証コード照合に成功する間は、仮ログインという状態で、SUBSESSION_KEY をセッション変数に発行している。処理はセッションとほぼ同じである。

plogin.php

 828: /**
 829:  * 仮ログイン状態かどうか
 830:  * @param   string $errmsg エラーメッセージ格納用(無ければ空文字)
 831:  * @return  bool TRUE:ログイン状態/FALSE:ではない
 832: */
 833: function issublogged(&$errmsg) {
 834:     $res = FALSE;
 835:     if (isset($_SESSION[SUBSESSION_KEY]) && ($_SESSION[SUBSESSION_KEY!'')) {
 836:         if (getLoggedStatus($_SESSION[SUBSESSION_KEY], $errmsg) == 4) {
 837:             $res = TRUE;        // 仮ログイン中
 838:         }
 839:     }
 840:     return $res;
 841: }

plogin.php

 715: /**
 716:  * データベースからログイン状態を取得
 717:  * @param   string $id     ID
 718:  * @param   string $errmsg エラーメッセージ格納用(無ければ空文字)
 719:  * @return  int  0:セッション破棄
 720:  *               1:セッション維持
 721:  *               2:二重ログイン
 722:  *               3:エラー
 723:  *               4:2要素認証待ち
 724: */
 725: function getLoggedStatus($id, &$errmsg) {
 726:     $errmsg = '';
 727:     if ($res = DBgetSession($id, $session, $dt, $errmsg)) {
 728:         $dt1 = new DateTime('now');
 729:         $dt2 = new DateTime($dt);
 730:         $t2 = dateInterval2Seconds($dt1->diff($dt2, TRUE));
 731:         if ($t2 > (SESSION_LIFE * 60)) {
 732:             DBsetSession($id, '', '', $errmsg);
 733:             unset($_SESSION[SESSION_KEY]);
 734:             unset($_SESSION[SUBSESSION_KEY]);
 735:             $res = 0;
 736:         } else if (($session !''&& ($session !session_id())) {
 737:             $errmsg = $id . ' はすでにログインしています';
 738:             // セッション継続用
 739:             $dt  = date(DATE_W3C, time());
 740:             $res = DBsetSession($id, $session, $dt, $errmsg);
 741:             $res = 2;
 742:         } else if (isset($_SESSION[SESSION_KEY])) {
 743:             $session = session_id();
 744:             $dt  = date(DATE_W3C, time());
 745:             $res = DBsetSession($id, $session, $dt, $errmsg);
 746:             $res = $res ? 1 : 3;
 747:         } else if (isset($_SESSION[SUBSESSION_KEY])) {
 748:             $session = session_id();
 749:             $dt      = date(DATE_W3C, time());
 750:             $res = DBsetSession($id, $session, $dt, $errmsg);
 751:             $res = $res ? 4 : 3;
 752:         } else {
 753:             $res = 0;
 754:         }
 755:     } else {
 756:         $res = 3;
 757:     }
 758:     return $res;
 759: }

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

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

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

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

解説:ログイン/ログアウト

plogin.php

 784: /**
 785:  * 正式にログインする
 786:  * @param   string $id     ID
 787:  * @param   string $errmsg エラーメッセージ格納用(無ければ空文字)
 788:  * @return  bool TRUE:ログイン成功/FALSE:失敗
 789: */
 790: function login($id, &$errmsg) {
 791:     $res = getLoggedStatus($id, $errmsg);
 792:     if (($res < 2|| ($res === 4)) {
 793:         $session = session_id();
 794:         $dt = date(DATE_W3C, time());
 795:         $res = DBsetSession($id, $session, $dt, $errmsg);
 796:         if ($res == TRUE) {
 797:             $_SESSION[SESSION_KEY] = $id;
 798:             unset($_SESSION[SUBSESSION_KEY]);
 799:         }
 800:     } else {
 801:         $res = FALSE;
 802:     }
 803:     return $res;
 804: }

plogin.php

 806: /**
 807:  * ログアウトする
 808:  * @param   string $errmsg エラーメッセージ格納用(無ければ空文字)
 809:  * @return  bool TRUE:登録成功/FALSE:失敗
 810: */
 811: function logout(&$errmsg) {
 812:     unset($_SESSION[SUBSESSION_KEY]);
 813:     $errmsg = '';
 814:     $res = TRUE;
 815:     if (isset($_SESSION[SESSION_KEY])) {
 816:         $id = $_SESSION[SESSION_KEY];
 817:         if ($id !'') {
 818:         $dt = date(DATE_W3C, time());
 819:         $res = DBsetSession($id, '', $dt, $errmsg);
 820:             if ($res == TRUE)   unset($_SESSION[SESSION_KEY]);
 821:         } else {
 822:             $res = FALSE;
 823:         }
 824:     }
 825:     return $res;
 826: }

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

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

解説:メイン・プログラム

plogin.php

1567: // インスタンス生成
1568: $pga = new pahooGoogleAuthenticator();
1569: 
1570: // セッション開始
1571: session_set_cookie_params(SESSION_LIFE * 60);
1572: session_start();
1573: 
1574: // DB初期化
1575: if (DBinit() == FALSE) {
1576:     $title  = TITLE_RESULT;
1577:     $errmsg = 'データベースの初期化に失敗しました';
1578:     $html = makeHTMLheader($title. makeHTMLresult('', $errmsg. makeHTMLfooter();
1579:     echo $html;
1580:     exit(1);
1581: }
1582: 
1583: // ID取得
1584: $errmsg = '';
1585: $idPat = array(ID_PATTERN);
1586: $id = getValidString('id', $errmsg, '', ID_MIN, ID_MAX, TRUE, $idPat);
1587: if ($errmsg !'') {
1588:     $title = TITLE_ERROR;
1589:     $html  = makeHTMLheader($title. makeHTMLresult($title, $errmsg. makeHTMLfooter();
1590:     echo $html;
1591:     exit(1);
1592: }
1593: 
1594: // パスワード取得
1595: $errmsg = '';
1596: $pswPat = array(PSW_PATTERN);
1597: $psw = getValidString('psw', $errmsg, '', PSW_MIN, PSW_MAX, TRUE, $pswPat);
1598: if ($errmsg !== '') {
1599:     $title = TITLE_ERROR;
1600:     $html  = makeHTMLheader($title. makeHTMLresult($title, $errmsg. makeHTMLfooter();
1601:     echo $html;
1602:     exit(1);
1603: }
1604: 
1605: // 認証コード取得
1606: $errmsg = '';
1607: $codePat = array(CODE_PATTERN);
1608: $code = getValidString('code', $errmsg, '', CODE_MIN, CODE_MAX, TRUE, $codePat);
1609: if ($errmsg !== '') {
1610:     $title = TITLE_ERROR;
1611:     $html  = makeHTMLheader($title. makeHTMLresult($title, $errmsg. makeHTMLfooter();
1612:     echo $html;
1613:     exit(1);
1614: }
1615: 
1616: // 動作モード取得
1617: $mode = getParam('mode', FALSE, '');
1618: // ログアウト
1619: if ($mode == 'logout') {
1620:     logout($errmsg);
1621: // 仮ログイン中
1622: else if (issublogged($errmsg)) {
1623:     $mode = 'sublogged';
1624:     $id = $_SESSION[SUBSESSION_KEY];
1625:     if (isButton('login2')) {
1626:         $mode = 'login2';
1627:     }
1628: // ログイン中
1629: else if (islogged($errmsg)) {
1630:     $mode = 'logged';
1631:     $id = $_SESSION[SESSION_KEY];
1632: else if (isButton('checkdup')) {
1633:     $mode = 'checkdup';
1634: else if (isButton('selsected')) {
1635:     $mode = 'selsected';
1636: else if (isButton('addsecret')) {
1637:     $mode = 'addsecret';
1638: else if (isButton('verify')) {
1639:     $mode = 'verify';
1640: else if (isButton('list')) {
1641:     $mode = 'list';
1642: }
1643: 
1644: $html = $errmsg = '';
1645: $result = FALSE;
1646: 
1647: // ログイン中
1648: if ($mode === 'logged') {
1649:     //
1650:     // ここに,ログイン中に実行したい処理を書いてください.
1651:     //
1652:     $title = TITLE_STATUS;
1653:     $msg = $_SESSION[SESSION_KEY. ' でログイン中です';
1654:     $html = makeHTMLheader($title. makeHTMLapplication($title, $msg. makeHTMLfooter();
1655: 
1656: // ログアウト
1657: else if ($mode === 'logout') {
1658:     $title = TITLE_STATUS;
1659:     $msg = 'ログアウトしました';
1660:     $html = makeHTMLheader($title. makeHTMLresult($title, $msg. makeHTMLfooter();
1661: 
1662: // 登録情報一覧
1663: else if ($mode === 'list') {
1664:     $title = TITLE_LIST;
1665:     $users = array();
1666:     DBlist($users, $errmsg);
1667:     $html = makeHTMLheader($title. makeHTMLlist($users, $errmsg. makeHTMLfooter();
1668: 
1669: // ID重複チェック
1670: else if ($mode === 'checkdup') {
1671:     DBcheckdup($id, $checkmsg);
1672:     $title = TITLE_SIGNUP;
1673:     $html = makeHTMLheader($title. makeHTMLsignup($id, $checkmsg. makeHTMLfooter();
1674: 
1675: // サインアップ(ID、パスワード入力)
1676: else if ($mode === 'signup') {
1677:     $title = TITLE_SIGNUP;
1678:     $checkmsg = '';
1679:     $html = makeHTMLheader($title. makeHTMLsignup($checkmsg, $id, $errmsg. makeHTMLfooter();
1680: 
1681: // 2要素認証を設定するかどうか
1682: else if ($mode == 'selsected') {
1683:     $title = TITLE_selsected;
1684:     // ID, パスワードをDBに登録
1685:     $res = DBaddAccount($id, $psw, $errmsg);
1686:     if ($res) {
1687:         $html = makeHTMLheader($title. makeHTMLselsecret($id. makeHTMLfooter();
1688:     } else {
1689:         $title = TITLE_SIGNUP;
1690:         $html = makeHTMLheader($title. makeHTMLsignup($id, $errmsg. makeHTMLfooter();
1691:     }
1692: 
1693: // 2要素認証を登録する.
1694: else if ($mode == 'addsecret') {
1695:     $title = TITLE_ADDSECRET;
1696:     // シークレット・キー生成
1697:     $secret = $pga->createSecret();
1698:     // シークレット・キーをDB登録
1699:     $res = DBaddSecret($id, $secret, $errmsg);
1700:     $html  = makeHTMLheader($title. makeHTMLaddsecret($id, $secret, $pga. makeHTMLfooter();
1701: 
1702: // ログイン(結果)
1703: else if ($mode === 'verify') {
1704:     $res1 = DBverifyAccount($id, $psw, $errmsg1);
1705:     $res2 = DBisSecret($id, $errmsg2);
1706:     if ($res1) {
1707:         if ($errmsg2 === '') {
1708:             // ログイン
1709:             if ($res2 === FALSE) {
1710:                 $res3 = login($id, $errmsg);
1711:                 if ($res3) {
1712:                     $title = TITLE_STATUS;
1713:                     $msg = $id . ' でログインしました';
1714:                     $html = makeHTMLheader($title. makeHTMLapplication($title, $msg. makeHTMLfooter();
1715:                 } else {
1716:                     $title = TITLE_LOGIN;
1717:                     $html = makeHTMLheader($title. makeHTMLlogin($id, $errmsg. makeHTMLfooter();
1718:                 }
1719:             } else {
1720:                 $res3 = sublogin($id, $errmsg);
1721:                 $title = TITLE_CODE;
1722:                 $html = makeHTMLheader($title. makeHTMLcode($id, $errmsg. makeHTMLfooter();
1723:             }
1724:         } else {
1725:             $html = makeHTMLheader($title. makeHTMLresult($title, $errmsg2. makeHTMLfooter();
1726:         }
1727:     } else {
1728:         $title = TITLE_STATUS;
1729:         $html = makeHTMLheader($title. makeHTMLresult($title, $errmsg1. makeHTMLfooter();
1730:     }
1731: 
1732: // 認証キー入力
1733: else if ($mode === 'sublogged') {
1734:     $title = TITLE_CODE;
1735:     $html = makeHTMLheader($title. makeHTMLcode($id, $errmsg. makeHTMLfooter();
1736: 
1737: // 認証キー入力
1738: else if ($mode === 'login2') {
1739:     $res1 = DBverifyCode($id, $code, $pga, $errmsg);
1740:     if ($res1) {
1741:         $res2 = login($id, $errmsg);
1742:         $title = TITLE_STATUS;
1743:         $msg = $id . ' でログインしました';
1744:         $html = makeHTMLheader($title. makeHTMLresult($title, $msg. makeHTMLfooter();
1745:     } else {
1746:         $title = TITLE_LOGIN;
1747:         $html = makeHTMLheader($title. makeHTMLlogin($id, $errmsg. makeHTMLfooter();
1748:     }
1749: 
1750: // ログイン(入力)
1751: else {
1752:     $title = TITLE_LOGIN;
1753:     $html = makeHTMLheader($title. makeHTMLlogin($id, $errmsg. makeHTMLfooter();
1754: }
1755: 
1756: // 表示処理
1757: echo $html;
1758: 
1759: // インスタンス解放
1760: $pga = NULL;

メイン・プログラムは、冒頭の2つのフローを1本の流れにまとめている。
セッションの状態と、画面遷移の状態を表す変数 $mode によって画面を切り替える。

参考サイト

(この項おわり)
header