目次
サンプル・プログラムのダウンロード
| plogin.php | サンプル・プログラム |
| pahooInputData.php | データ入力に関わる関数群。 使い方は「数値入力とバリデーション」「文字入力とバリデーション」などを参照。include_path が通ったディレクトリに配置すること。 |
| pahooQRimage.php | QRコード(2次元バーコード)作成に関わる暮らす。 使い方は「PHPでQRコードをつくる」などを参照。include_path が通ったディレクトリに配置すること。 |
| バージョン | 更新日 | 内容 |
|---|---|---|
| 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対応 |
| バージョン | 更新日 | 内容 |
|---|---|---|
| 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() 追加 |
| バージョン | 更新日 | 内容 |
|---|---|---|
| 1.0.1 | 2025/07/29 | text2QRimage() -- bug-fix |
| 1.0.0 | 2025/07/29 | 初版 |
サインアップの流れ
プログラムは1本のPHPファイルだが、セッションの有無、POST で渡される mode の値によって、機能別の画面を表示できるようにしてある。
「サインアップ」をクリックすると、サインアップ処理に入る
ログインの流れ
ID、パスワードを入力し、「ログイン」ボタンをクリックする。
実際のアプリケーションでは、この画面を雛形として処理を進める。ログイン状態はセッション SESSION_KEY に保管されている。
Google Authenticator を使い、該当するアカウントの認証コード(数字6桁)を入力する。
登録情報一覧
通常のログイン機能では必要のない画面だが、パスワードがハッシュ化されていることを明らかにするための参照機能として用意した。
余談になるが、アカウントの認証に用いるパスワードやシークレットキーは、企業トップやシステム管理者が把握してはいけないと考えている。それを可能にすれば、なりすましができてしまうからだ。何かトラブルが起きたとき、運用ルールで制約を欠けているだけでは不十分で、確実になりすましができないことを証明する意味で、このような仕様にした。
もしアカウント利用者がパスワードやシークレットキーを忘れてしまったら、面倒でも再発行(再サイインン)させるべきだ。
準備:pahooInputData 関数群
また、各種クラウドサービスに登録したときに取得するアカウント情報、アプリケーションパスワードなどを登録した .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コードを生成するのに、"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_PATTERN や PSW_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 ===========================================================
シークレットキー(セットアップキー)の生成、認証コードの生成と照合などを行う。
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: }
本プログラム起動時にデータベースファイルが無ければ、DBinit を呼び出す。使用するテーブルは user の1つだけで、その構造はコメントに記載したとおりだ。
解説:ログイン状態の管理
今回は、次の条件でログイン状態かどうかを判断する。
- セッション変数 $_SESSION[SESSION_KEY] にIDがあるかどうか
- データベースのセッションIDと一致するかどうか
- データベースの最終アクセス日時と現在時刻の差が 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)) {
メインプログラムの冒頭に上述のように記載することで、ログイン状態かどうかの判定を行うことができる。
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: }
まず、ユーザー関数 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: }
ログアウト処理を行う関数 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;
セッションの状態と、画面遷移の状態を表す変数 $mode によって画面を切り替える。
参考サイト
- PHPセキュリティ対策:パスワードとcrypt関数:ぱふぅ家のホームページ
- 2018年のパスワードハッシュ:Qiita
- PHPセキュリティー 認証と認可:WEPICKS!
- 【PHP】ログイン_サンプルコード(Login form sample):忘れんうちに書いとけ

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)に対応,大幅改訂