Apache + PHP + PostgreSQL で認証ページを作る (3)

(3) セッション管理機能をつける。

これまでは apache の basic 認証の仕組みを利用していましたが、しょせんは basic 認証。一旦認証されてしまうとブラウザを閉じるまでは有効ですし、別のユーザで入り直すなんてこともできません。

これだと、普段は一般ユーザとして利用して、あるときには管理者としてログインしたい場合には不便です。しかしそれにはセッション管理の機能がなければ安心して使うことができません。

PHP4 以降には基本的なセッション管理の仕組みがありますので、これを利用してみましょう。

なお、セッション情報そのものも PostgreSQL で管理することもできますが、今回は認証のみに利用し、セッション情報はサーバのローカルファイルに置くことにします。レンタルサーバなど local exploit に対する risk 管理が必要な場合にはデータベースに置いた方がいいでしょう(ただし、StackAsterisk の記事に、PostgreSQL でセッション情報を利用するとスケールし難い問題が指摘されていますので、この場合にはご一読をお薦めします)

まずはログインするフォームです。テスト用なのでかなりしょぼいです。

PHP を使う際に EUC-JP を文字コードに指定しているので、文字コードは EUC-JP で作成します。私の場合には Windows 上で DOS 用 QKC で先に EUC に変換したファイルを Wz Editor 5.0 (EUC対応)で開いています。

<HTML>
<HEAD>
   <TITLE>Login</Title>
   <META HTTP-EQUIV="Content-Type" CONTENT="text/html; charset=euc_jp">
</HEAD>
<body>
<form action="auth.php" method="POST">
        ユーザー名 : <input type="text" size="16" name="f_username"><br>
        パスワード : <input type="password" size="16" name="f_passwd"><br>
        <input type="submit" value="ログイン">
        <input type="reset" value="入力取消">
</form>
</body>
</html>

ここからデータを受け取るのが以下の PHP ファイルです。名前は auth.php にしています。認証に成功すれば main.php にジャンプする設計です。

<?php
/* まず POST 変数の受けとり。
   PHP 4.2.0 以降はグローバル変数でいきなりPOSTで受け取った内容を
   使うような下品なことはデフォルトでできないようになりました */
$f_username = $_POST['f_username'];
$f_passwd = $_POST['f_passwd'];

/* user 名が空だったら SQL 回すまでもないのでトラップ */
if ( $f_username == "" ) {
echo  <<< EOT
<HTML>
<HEAD>
   <TITLE>Authentication Falure</Title>
   <META HTTP-EQUIV="Content-Type" CONTENT="text/html; charset=euc_jp">
</HEAD>
<body>
Authentication Failure : Cannot specify your username.<br>
<br>
ユーザ名が指定されていません。認証は失敗しました。<br>
<br>
<br>
<div align="center">
<a href="login.php">戻る</a></div>
EOT;
exit;
}

/* ここからセッション開始 */
session_start();

@$con = pg_connect("dbname=mod_auth_pgsql");
if ($con == false) {
    print("認証データベースへの接続に失敗しました。理由;$php_errormsg<br>\n");
    exit;
}
$table = "users";  /* ユーザ情報が入っているテーブル名 */
$unamefield = "name"; /* ユーザ名が入っているフィールド名 */
$pwfield = "pass"; /* パスワードフィールド名 */

$sql = "SELECT * FROM $table WHERE $unamefield LIKE '" . addslashes($f_username) . "'";
@$result = pg_query($con,$sql);
if ($result == false) {
    print("SQL文 $sql が実行できませんでした。理由:" . pg_errormessage($con));
    sessin_destroy();
    exit;
}

if ( pg_numrows($result) > 1) {
/* ユーザ名が重複している? */
    echo <<< EOT
<HTML>
<HEAD>
<TITLE>Authentication Falure</Title>
<META HTTP-EQUIV="Content-Type" CONTENT="text/html; charset=euc_jp">
</HEAD>
<body>
Authentication Failure<br>
<br>
データベース内部エラーにより認証は失敗しました。管理者に連絡してください。<br>
<br>
<br>
<div align="center">
<a href="login.php">戻る</a></div>
</body></html>
EOT;
    session_destroy();
    exit;
}

$d_passwd = pg_result($result,0,$pwfield);

/* 該当するユーザ名がない場合には NULL が返ってくる */

if ( ($d_passwd == NULL) || ($f_passwd != $d_passwd) ) {
    /* パスワードが一致しない */
    echo <<< EOT
<HTML>
<HEAD>
<TITLE>Authentication Falure</Title>
<META HTTP-EQUIV="Content-Type" CONTENT="text/html; charset=euc_jp">
</HEAD>
<body>
Authentication Failure<br>
<br>
認証は失敗しました。<br>
<br>
<div align="center">
<a href="login.php">戻る</a></div>
EOT;
    session_destroy();
    exit;
}

/* ここまできたら認証成功の時の処理を行う */
/* まずセッション変数の登録 */
/* もちろんパスワードなんかは登録しない */

session_register($ses_username);
$ses_username = $f_username;

/* 自動的に main.php へ画面遷移する */

header("Location: main.php");
?>

/* Location ヘッダを認識してくれないブラウザの場合にはリンクを表示 */

<html>
<head></head>
<body>
Authentication Completed!<br>
<br>
認証に成功しました。<br>
<br>
<a href="main.php">進む</a>
</body>
</html>

しこたま日本語でコメントを書き込んだのでだいたいどういうことを考えて作ったかは分かって頂けると思いますが、初歩的な点でつまずいたところをいくつか挙げておきましょう。

1.POSTパラメータが受け取れない

結局、原因は参考にしていたテキストの該当部分がグローバル変数受け渡しを利用していたのが原因だったんですが、「POSTパラメータが受け取れない」だけをキーワードに google とかで探してもなかなか正解に行き当たりませんでした。

2.日本語コード

原稿は Windows 上で書くことが多いので Shift-JIS にしようと思ったんですが、考えてみたら PHP のデフォルトの日本語コードは EUC にしているじゃないですか。そこで、一旦 DOS のツールでソースを EUC にコード変換してから使うことにしました。

3.HTMLの規約

ヒアドキュメントを利用して HTML のタグを出力している部分がたくさんありますが、なんどやってもブラウザには <HTML><BODY></BODY></HTML> しか出力されないことがありました。調べたら原因はヒアドキュメントにした HTML 部分を字下げしていたこと。当たり前といえばそうなのですが、<HTML> の前にはたとえブランクであろうがタブであろうが出力してはいけないのを忘れていました。

4.ユーザ指定変数をSQLにかけるときには

あれほどうるさくセキュリティの記事を書いている割にはすっかり忘れそうでした。ということで、ちゃんと addslashes しましょう。認証DBが丸裸なんて格好悪すぎます。

5.同じユーザIDの人がいたら

テキストを流し読みしていて、pg_numrows という関数が目にとまり、このケースだとどうなるかふと考えたところ、ロジックの見落としに気付きました。認証DBを pgsql で \d users とすると分かりますが、name フィールドは not NULL ではありますが、unique 属性はつけていません。これは解説でも話した通り、将来的に複数のアプリケーションで認証したい場合にユーザ名の重複を許容するためでした。しかし、このままだと foo というユーザのデータを2つ作れてしまいます。そこで、2レコード以上マッチした場合にはエラーにするような処理を入れておきました。本来ならデータベースの仕様をきっちり固めるのが正攻法でしょう。pg_numrows == 0 の場合の処理はその下でやっています。このロジックもあやうく抜けるところでした。このソースではエラー条件を全部弾いたら認証成功というロジックなので、抜けがあると致命的です。本来は逆の方がいいのですが…。

6.エラーが画面に出ない

ちゃんと調べていませんが、エラーメッセージがセキュリティホールに成りえることから、昔とは違っておそらく意図的にエラーメッセージを画面上に出さないような設定がデフォルトでされているようです。ブラウザ画面には出ませんが、apache の error.log にいろいろメッセージが出ますのでこちらを見ましょう。ISPのサーバを使っていてエラーが見れない場合は、手元に実験環境を用意した方が後々のデバッグのためかもしれません。



認証の中心になる部分はこれで一応機能するようになりましたが、これはあくまで「とりあえず動く」レベルです。

これを元に機能追加を考えていきたいと思います。

考えているネタとしては…。
・管理者ユーザを決めて、管理者でログインした場合にDBのメンテナンスができる。

・管理者でない場合でも自分のパスワードを変更できる。

・エラーページには HTTP の 401 などのコードも返すようにする。

・パスワードはハッシュ化して復号できないようにする。

・ログに変更履歴を残すようにする。
こんな感じです。できるところから続編を公開する予定です。

前へ