[ホーム] -> [Aache + PHP + PostgreSQL 実験室]

PostgreSQL で Apache のユーザ認証

ユーザ毎の認証

先ほど説明したパスワードファイルを使ったユーザ認証ですが、ユーザ数が多くなるとだんだんパフォーマンスが悪くなるのでお勧めできません。基本認証の仕組みから言うと、ユーザがユーザ名/パスワードを入力した時ではなく、ユーザ認証が必要な部分へアクセスするたびにパスワードファイルがチェックされます。したがって何千人ものユーザがいる場合は、ページにアクセスするたびにパスワードファイルをすべて検索しユーザが存在するかを調べるのです。これはかなりまずいことなので、Apache のマニュアルだと DBM ファイルを使ったユーザ管理をするようにと書いてあります。が、ここでは PostgreSQL を使った説明をしているので、PostgreSQL を使ったユーザ認証について説明したいと思います。

ユーザ認証を PostgreSQL でするようにするために、mod_auth_pgsql というモジュールが公開されています(いや、別に自分で CGI 作ってもいいですけどね)。使い方は簡単で、ドキュメントを読めば出来ると思いますが、初期設定が手間なので、サンプルがてら説明したいと思います。

まず、当然モジュールを手に入れてコンパイルし、Apache に組み込みます。

次に、このモジュールを利用できるように、テーブルを作る必要があります。このモジュールが利用するテーブルは3つあります。ユーザテーブル、グループテーブル、ログテーブルです。ユーザテーブルは必須ですが、他の2つは任意です。まずユーザテーブルの説明を行いましょう。

create table auth_user (
  userid    varchar(40)  primary key,
  password  varchar(32),
  lock      integer      default 1
);

必須のカラムは、ユーザ名(例では userid)と、パスワード(例では password)です。テーブル名、カラム名は何でもいいし、カラムのデータ型も文字列系であれば大丈夫です。ユーザ名は重複してはいけないので、primary key を設定しておきます。lock というカラムを付けましたが、これは後の説明の為に付けました(本当は必要ない)。

格納しておくパスワードは、クリアテキスト(パスワードそのまま)、crypt 暗号、MD5 の3種類から出来ます。crypt の場合は13バイト、MD5の場合は32バイト必要なので、ここでは 32 バイトにしました。

次に、このテーブルを認証で使うように、mod_auth_pgsql の設定を行います。

## ユーザ認証をするときに必要な設定
AuthName "PostgreSQL Authenticator"
AuthType Basic
# 認証可能なら誰でも。(もちろん、「user ユーザ名」も使えます)
Require  valid-user

## mod_auth_pgsql の設定
# PostgreSQL のサーバ(UNIX ドメイン経由の場合は必要ない)
#Auth_PG_host            localhost
# PostgreSQL のポート(特に変更してなければ必要ない)
#Auth_PG_port            5432
# データベース名
Auth_PG_database        wwwdb
# データベースに接続するユーザ名
Auth_PG_user            wwwuser
# データベースに接続するユーザのパスワード(設定してなければ必要ない)
Auth_PG_pwd             wwwpass
# 先ほど作成したテーブル名
Auth_PG_pwd_table       auth_user
# 先ほど作成したテーブルのユーザ名が入っているカラム名
Auth_PG_uid_field       userid
# 先ほど作成したテーブルのパスワードが入っているカラム名
Auth_PG_pwd_field       password
# パスワードがクリアテキストの場合は Off(デフォルトOn)
Auth_PG_encrypted       Off

一応上記の設定をすれば、ユーザ認証が出来るようになると思います。試しにデータを追加してログインしてみましょう。

insert into auth_user values
  ('Clear', 'Pass' , 0);

出来ましたか? クリアテキストの設定になっているので、ユーザ名「Clear」パスワード「Pass」でログインできるはずです。

内部的な処理ですが、入力されたユーザ名を元にユーザテーブルを検索し、存在した場合はそのパスワードと入力されたパスワードが一致するかを調べているようです。従って、ユーザ名が一致しない限り、パスワードは評価されないため、パスワードにはインデックスは必要ありません。

パスワードの暗号化

クリアテキストで格納しておくのはちょっと、と言う人のために、crypt と MD5 のデータを作ってみます。パスワードを「Pass」とした場合、どういう風に作ればいいかの一例を示します。

crypt の場合(Perl を使う)
> perl -e "print crypt('Pass',join('',('.','/',0..9,'A'..'Z','a'..'z')[rand 64, rand 64]))."'"\n";'
MD5 の場合(md5sum を使う、echo -n が改行されないのが前提)
> echo -n 'Pass' | md5sum

MD5 は毎回同じ値になりますが、crypt は salt をランダムで求めているので、毎回違う値になると思います。また、MD5 は大文字小文字どちらでも大丈夫です(大文字小文字無視して一致するかを調べている)。作成した結果をテーブルに登録しておきます。

insert into auth_user values
  ('Crypt', 'XaeZe7PpKnmJc'                   , 0);
insert into auth_user values
  ('MD5'  , 'b9b57aae83585e17ede4570dcede353c', 0);

次に mod_auth_pgsql の設定を変更します

# 暗号化パスワードを使う場合は On
Auth_PG_encrypted       On
# パスワードの暗号化方法を指定(MD5 か Crypt)
Auth_PG_hash_type       Crypt
#Auth_PG_hash_type       MD5

暗号化方法を変えてログインして試してみてください。どちらでもいいのでしたら、当然 MD5 をお勧めします。Crypt はパスワードを 8 文字までしか意味がないし、最近のマシンパワーを考えると総当たりで調べ切れてしまうでしょうからね。

グループ毎の認証

グループテーブルを使った認証方法の説明です。今までの説明は、ユーザテーブルに存在するユーザならすべてがログイン可能でしたが、今度はグループに登録してあるユーザのみ許可、と言う風にしてみようと思います。

create table auth_group (
  groupid   varchar(40),
  userid    varchar(40) references auth_user(userid) on delete cascade,
  lock      integer     default 1,
  primary key (groupid, userid)
);

必要な項目は、グループ名(例では groupid)とユーザ名(例では userid)で、ユーザ名はユーザテーブルに使ったのと同じカラム名でなければなりません。テーブル名やグループ名は何でも構いません。

ユーザ名は、ユーザテーブルのユーザ名と関連づけられているので、references auth_user(userid) on delete cascade というオプションを付けました。これは、この auth_group テーブルに、auth_user テーブルの userid カラムに存在するデータしか入力することが出来ないような設定です(references auth_user(userid) の部分)。さらに、auth_user テーブルから同じユーザ名が削除されたら、auth_group テーブルからも自動で削除します(on delete cascade)。

また、プライマリーキーは、グループ名とユーザ名のペアです。あるグループに対してユーザを複数登録できますが、ある一つのグループに対しては、ユーザが重複することはあり得ません(あっても無意味)。なので、この様に組み合わせてプライマリーキーにしています。組み合わせる場合は、この様にすべてのカラム定義の最後に、primary key (groupid, userid) と、組み合わせる順でカラム名を指定します。

テーブル登録が終わったら、ユーザとグループを登録してみます。

insert into auth_user values ('Gp1User1', 'Pass', 0);
insert into auth_user values ('Gp1User2', 'Pass', 0);
insert into auth_user values ('Gp1User3', 'Pass', 0);
insert into auth_user values ('Gp2User1', 'Pass', 0);
insert into auth_user values ('Gp2User2', 'Pass', 0);
insert into auth_user values ('Gp3User1', 'Pass', 0);
insert into  auth_group values ('Gp1', 'Gp1User1', 0);
insert into  auth_group values ('Gp1', 'Gp1User2', 0);
insert into  auth_group values ('Gp1', 'Gp1User3', 0);
insert into  auth_group values ('Gp2', 'Gp2User1', 0);
insert into  auth_group values ('Gp2', 'Gp2User2', 0);
insert into  auth_group values ('Gp3', 'Gp2User1', 0);

試しに次のようにして、references auth_user(userid) on delete cascade がうまく動いているか調べてみてください。

存在しないユーザをグループテーブルに登録してみる
=> insert into  auth_group values ('Gp5', 'UnknownUser', 0);
ERROR:  <unnamed> referential integrity violation - key referenced from
auth_group not found in auth_user

グループテーブルを確認し、ユーザテーブルからユーザを削除
=> select * from auth_user where userid like 'Gp2%';
  userid  | password | lock
----------+----------+------
 Gp2User1 | Pass     |    0
 Gp2User2 | Pass     |    0
(2 rows)
=> delete from auth_user where userid = 'Gp2User1';
DELETE 1
=> select * from auth_user where userid like 'Gp2%';
  userid  | password | lock
----------+----------+------
 Gp2User2 | Pass     |    0
(1 rows)

次にグループテーブルを利用するように Apache の設定を変更します。

# グループテーブル名
Auth_PG_grp_table       auth_group
# グループテーブルのグループ名が入ったカラム名
Auth_PG_gid_field       groupid

# グループ名 Gp1 のユーザのみ許可
Require group Gp1

これでグループ名指定で許可を行えるようになりました。

ログの記録

mod_auth_pgsql には、ユーザがアクセスしたログを、テーブルに登録する機能も備えています。ログを格納するテーブルは、次のような感じになります。

create table auth_log (
  userid      varchar(40),
  accessdate  timestamp,
  accessuri   varchar(256),
  remoteaddr  varchar(128),
  password    varchar(32)
);

今回は特にプライマリーキーにするようなカラムはありませんので、特に何も付けずに作成します。これも他と変わらず、テーブル名、カラム名は何でも構いません。サイズは適当です。カラムの目的は順番に、ユーザ名、アクセスした日時、アクセス先の URI(メソッド パス プロトコルバージョン)、クライアントのアドレス、パスワードです。パスワードは暗号化しているかに関係なくクリアテキストで保存されるので、このカラムはなくてもいいでしょう。

このログデータですが、認証が成功したアクセスだけ記録されるということに、気を付けてください。失敗は記録されません。

Apache の設定は次のようになります。

# ログテーブル名
Auth_PG_log_table       auth_log
# ログテーブルのユーザ名
Auth_PG_log_uname_field userid
# ログテーブルのアクセス日付
Auth_PG_log_date_field  accessdate
# ログテーブルのアクセス URI
Auth_PG_log_uri_field   accessuri
# ログテーブルのクライアントアドレス
Auth_PG_log_addrs_field remoteaddr
# ログテーブルのパスワード
Auth_PG_log_pwd_field   password

記録が必要な項目だけを定義すればいいです。このログテーブルですが、Apache のログファイルと重複するので、どちらか一方だけ取ればいいと思いますが・・・。

その他のオプション

説明が後回しになっていた lock フィールドについて説明します。mod_auth_pgsql には Auth_PG_pwd_whereclause というディレクティブがあり、このディレクティブに設定した文字列が、テーブルを検索するときの where 句に追加されます。例えば次のように設定したとします。

# ユーザテーブル検索時に追加されるオプション
Auth_PG_pwd_whereclause " and lock != 1"
# グループテーブル検索時に追加されるオプション
Auth_PG_grp_whereclause " and lock != 1"

すると、この設定がない場合は「select password from auth_user where userid = 'User'」という SQL 文でユーザが検索されますが、この設定値が加わるため、「select password from auth_user where userid = 'User' and lock != 1」という SQL 文が実行されます。したがって、lock カラムに 1 が設定されていると、検索条件に合わないためユーザが存在しないと判断されます。

この仕組みにより、この例のようにユーザをデータベースに登録したままで利用不可にしたりできます。あるいは有効期限を設定しておく expire_date というカラムを用意し、「and expire_date >= current_date」とかして有効期限までしかログインできなくしたりも出来ます。複雑にしたい場合はカッコでくくり、「and ((expire_date is null or expire_date >= current_date) and (lock is null or lock != 1)」とか指定することも出来ます。

ちなみに、今まで Auth_PG_pwd_table, Auth_PG_grp_table にテーブル名を指定していましたが、user_base_data b left join user_ext_data e on (b.userid = e.userid) とか指定して、複数のテーブルを結合させることも出来ます。

実際のところ、ユーザテーブルを検索した結果、1 件だけ返すような SQL 文を作成できればいいのです。もし、2 件以上返してしまうと、エラーになってしまいますので注意してください。

残りのオプションも説明しておきましょう。ちなみに、Auth_PG_options 以外は、すべてデフォルト Off です。

# 接続時にバックエンドに渡すオプション
Auth_PG_options         "--debug_level=3"
# パスワードが空でも接続を許可する
Auth_PG_nopasswd        on
# ユーザ名をすべて小文字で検索
Auth_PG_lowercase_uid   on
# ユーザ名をすべて大文字で検索
Auth_PG_uppercase_uid   on
# パスワードを大文字小文字の区別無く検索(クリアテキストのみ)
Auth_PG_pwd_ignore_case on
# パスワードをキャッシュ
Auth_PG_cache_passwords on

Auth_PG_options は、これは PostgreSQL に接続したときに渡すオプションです。postgresql.conf で定義するオプションの一部を指定できます。どれが利用できるかはマニュアルを参照してください。

Auth_PG_lowercase_uid, Auth_PG_uppercase_uid は、ユーザが入力したユーザ名をそれぞれ大文字・小文字に変換してユーザテーブルを検索します。両方指定してある場合は、大文字小文字の両方検索してみてどちらかのパスワードが正しければ認証許可されます。ただし、どちらかでも指定されていると、ユーザが入力した文字そのものでは検索しないので、ユーザテーブル内のユーザ名が、大文字か小文字に統一されているときのみ指定した方がいいでしょう。

Auth_PG_pwd_ignore_case ですが、On の時はパスワードの比較を行うときに大文字小文字を無視して比較します。

Auth_PG_cache_passwords はパスワードをキャッシュする設定です。ユーザ名とパスワードが一致したものを内部で保持していて、それ以降はデータベースを見に行かなくなります。ただし、この数はデフォルト 50 ユーザ分で、それ以上になると一度キャッシュは破棄されます。デフォルトの 50 を変更したい場合は、apxs -DMAX_TABLE_LEN=100 ... とかオプションを付けてコンパイルし直す必要があります。キャッシュを多くすると効率が良くなりますが、パスワードの変更を行ったときなどなかなか反映されなくなるので注意してください。

総評

mod_auth_pgsql は、PostgreSQL によるユーザ管理が出来、各テーブルの柔軟に対応できるモジュールです。すでに PostgreSQL でユーザ管理を行っている場合や、そのようなシステムを作る場合は一考の価値があるかもしれません。

ただし、負荷の高いサイトは、きちんと負荷テストを行った方がいいと思います。と言うのも、パスワードを検索するたびに PostgreSQL に接続・切断するためです。基本認証を使っているので、認証を必要としているすべてのページにアクセスするたびに繰り返されることになります(Auth_PG_cache_passwordsOff の場合)。当然、画像やスタイルシートのファイルなどを認証付きのディレクトリ下に置いてある場合、それらすべてに対してこの作業が行われます。これを避けるには、mod_auth_pgsql を改造するしかないですね。ただ、手軽に PostgreSQL 認証を行えるという点で、非常に有用です。

ホームへ