Friction River Software

  • お問い合わせ

CakePHP5入門【CakePHP5認証編③】ログインステータス

A子

各ユーザのログイン状況を確認できるようにするため、データベーステーブルを変更しよう
(つまり、「users」テーブルに項目を追加するよ)

C菜

前回、B美部長がおっしゃったように「status」という項目を追加するんですか~?

A子

うん、それで良いよ
(名前を考えるのが面倒くさい)

B美

だったらついでにもう一つ、文字列(varchar)型の項目を追加しておきなさい

項目名は「session」で…

A子

ん?
セッション?

そこには何を格納するのさ?

C菜

そもそもの話ですけど~

セッション」の仕組みって、どうなってるんですか~?

B美

手順としてはこうなるわ

1.ブラウザがWebサーバにアクセス
2.Webサーバは重複しない「セッションID」を作成し、ブラウザへ送信
3.ブラウザはその「セッションID」をCookieクッキーとして保存
4.同じWebサーバの別ページへアクセスする際、そのCookieクッキーをWebサーバへ自動送信
5.Webサーバは「セッションID」を照合して、さっきと同じユーザであることを認識
6.無操作の状態が一定時間続くと、Webサーバは「セッションID」を無効化する(これが「セッションタイムアウト」)
7.操作している限り、「セッションID」は無効化されない

A子

くっきー…って何だっけ?

よく聞く言葉だけど、あまり分かってないんだよね(苦笑)

C菜

Webサーバ側で作ったデータをブラウザ側に持たせておく仕組みですよ~

あるWebサーバから受け取ったCookieクッキーは、再び同じWebサーバにアクセスするときに自動送信されるって仕組みのはずです~

A子

なるほどねぇ

それによって、Webサーバは「アクセス元が同じユーザかどうか」を見極めることができるってわけか…

B美

そういうことよ
一つ付け加えておくなら「Sessionセッションはサーバ側に、Cookieクッキーはクライアント側に保存される」…ってことね

んで、Webサーバはファイル名として「セッションID」の付いたものを保存してるんだけど、タイムアウトしたらそのファイルは削除されるの

C菜

あっ!

でしたら、そのファイルがあるか否かを「file_exists」関数で調べることで、「セッションタイムアウト」になっているかを判別することができますね~

B美

ご名答!

だから、データベーステーブル内に現在の「セッションID」を格納しておきたいのよ

A子

セッションID」って簡単に取得できるの?

B美

もちろん!

session_id」という関数を呼び出すだけよ
(これは普通にPHPの関数です)

まぁ、CakePHPで推奨される書き方としては、下記のようになるけどね

$this->request->getSession()->id()

ただ

session_id()

という、単なる関数呼出しを使っても全く問題ないわよ
(得られる結果は同じだし…)

A子

ふむふむ
それじゃ、さっそくやっていこう

まずはデータベーステーブルを変更するよ

あ、でも「テーブル定義を一度削除してから、あらためて新規作成」するのではなく、「テーブル定義を修正」する形でやりたいんだよね
(すでにユーザ登録してるし…)

B美

だったら「ALTER TABLE」を使いなさい

ググれば簡単に調べられるから…

A子

ちぇっ、面倒だけど検索するかぁ

・・・

うん、こんな感じかな

ALTER TABLE users
    ADD status int AFTER password,
    ADD session varchar(255) DEFAULT NULL AFTER status;

これで「password」と「created」の間に「status」と「session」が挿入されるはず…
(あ、三行で書いてるけど、これって一文だからね)

C菜

上記のコマンドを実行後にテーブル定義を確認してみました~

A子

「src/Model/Entity」の中にある「User.php」と、「src/Model/Table」の中の「UsersTable.php」については、特に修正の必要は無さそうだね

C菜

ですね~

あ、でも一応キャッシュクリアだけは、やっておいたほうが良いかもです~

A子

あ、そうだった

cd html/authapp[Enter]
bin/cake cache clear_all[Enter]

まぁ、あくまでも念のため…だけどね

C菜

「src/Controller」の中にある「UsersController.php」はどうします~?

「login」と「logout」メソッドのことですけど~

A子

login」メソッドには以下のコードを追加しよう

$user = $this->Authentication->getIdentity();
$loginUser = $this->Users->get($user->id);
$loginUser->status = 1;    //ログイン
$loginUser->session = session_id();
$this->Users->save($loginUser);

次に「logout」メソッドの追加分はこうだね

$user = $this->Authentication->getIdentity();
$loginUser = $this->Users->get($user->id);
$loginUser->status = 0;    //ログアウト
$loginUser->session = null;
$this->Users->save($loginUser);

ログインの有無を表す「status」の値は、ログイン状態が「1」で、ログアウト状態が「0」(または「null」)ってことにしよう


B美

うーん、惜しい!

良い線いってるんだけどねぇ

C菜

どこに問題があるのでしょうか~?

B美

「logout」メソッドはOK
問題は「login」メソッドのほうね

何が問題か…って、そのタイミングでは「セッションID」をきちんと取得できないのよ
(要するに、topページへリダイレクトしたあと「セッションID」を取得するように!…ってこと)

A子

なるほど、なるほど
でも、そこまで分かれば何とかなるかな

「src/Controller」の中にある「TopController.php」を書き換えるよ
まずは「index」メソッドね
(そのユーザの「session」項目の値が「null」だったら、セッションIDを取得してデータベーステーブルを更新します)

public function index()
{
    $user = $this->Authentication->getIdentity();
    $loginUser = $this->Users->get($user->id);
    if (is_null($loginUser->session)) {
        // データベース上でログイン状態にする
        $loginUser->status = 1;    //ログイン
        $loginUser->session = session_id();
        $this->Users->save($loginUser);
    }
}

もちろん、「users」テーブルを使う準備も追加するよ

private $Users;

public function initialize(): void
{
    parent::initialize();

    $this->Users = $this->fetchTable('Users');
}

で、どうよ

A子

あ、「UsersController.php」の「login」メソッドについては、元の状態に戻しておいたよ

B美

あ、ちょっと待って!

「session」の値をnullにすることだけは「login」メソッド内でやっておきなさい

C菜

なぜでしょうか~?

B美

ログイン後、ログアウトせずにブラウザを閉じて、そのあとすぐにブラウザを起動して「top」ページにアクセスした場合、どうなると思う?

A子

ん?
ログイン状態のまま、topページが表示されるんじゃないの?

B美

違うわ

C菜

もう一度ログインページが表示されるんでしょうか~?

B美

そういうこと

そして「ブラウザ再起動を伴う再ログインを行った場合、セッションIDは再取得(つまり、さっきのとは異なる値)になる」のよ

A子

あ、分かった!
さっきのコードだと、データベーステーブルの「session」項目の値が更新されないことになるね
(だって「session」項目の値がnullじゃないし…)

…ってことは、最初のコードから「status = 1;」の行を削除して、「session」には「null」を代入すれば良いのかぁ

B美

ふむ
まぁ、良いんじゃないかしら

とりあえずテストしてみなさいな

C菜

まずは、現状の「users」テーブルの中身です~

A子

んじゃ、私のアカウント(ako@friction-river.jp)でログインして…っと

お、「users」テーブルの中身はこうなったよ
もっとも、セッションIDの値が正しいのかどうかは知らんけど(笑)

C菜

セッションファイルはどこに作られるんでしょうか~?

B美

「php.ini」を特に変更していなければ「/var/lib/php/sessions」ディレクトリの中よ

rootにsuしてから確かめてみなさい

A子

えっとー

su -[Enter]
cd /var/lib/php/sessions[Enter]
ls -l[Enter]

で、どう?

C菜

さきほどのデータベース内の値と比べると、ファイル名は「sess_(セッションID)」ということでしょうか~?

B美

大正解!

で、このセッションファイルは、「ログアウト」または「セッションタイムアウト」で自動的に削除されるってわけ

A子

やってみよう
まずは「ログアウト」して…っと

データベース内とセッションファイルを見てみると、こうなったよ


C菜

最初のセッションファイルについては消えましたけど、新たなファイルができてますね~

B美

これについては、無視しても構わないわ

ファイルサイズ(37バイト)でも分かる通り、意味のないファイルだから…

A子

セッションタイムアウトのときも同じようになるの?

B美

そこはちょっと注意が必要なんだけど…

タイムアウトの場合、セッションファイルがすぐに削除されるわけではない
一定時間ごとに「gc(ガーベージコレクション)」が働くんだけど、そのタイミングで削除されることになるのよ

A子

が、がぁべぃじ?

また意味不明なことを…(苦笑)

C菜

もしもつづりが「garbage collection」ならば、直訳して「ゴミ収集」でしょうか~?

B美

さすがはC菜、その通りよ

余談だけど、カタカナ表記する場合、「ガーベージ」「ガベージ」「ガーベジ」の三通りを見かけるわ
(ネット上や書籍で…)

正しい英語の発音的には「ガーベージ」だけどね

A子

で、それって何なのよ

「ゴミ収集」ってことは、不要なファイルを削除する仕組みってこと?

B美

その役割もあるんだけど、もともとの意味としては、コンピュータのメモリ内に存在する不要なオブジェクトを消去する仕組み

メモリ内に動的に(プログラム実行中に)生み出された確保領域って、使い終わったら消していかないとメモリを圧迫することになるの
言い換えれば、消し忘れがあるとメモリの空き容量が減っていくのよ
(これを「メモリリーク」と呼びます)

ガーベージコレクションというのは、それ(不要なオブジェクトの消去)を自動的に行ってくれる仕組みってわけ
(ほとんどのオブジェクト指向言語には備わっている仕組みよ…いや、この仕組みを持たないプログラム言語もあるんだけど(苦笑))

A子

ガーベージコレクションが働くタイミングは?

何分おきとか決まってるの?

B美

知らん!

気にしたことは無いし、気にする必要もないわ
(いつの間にか勝手に動いてる…って認識でOK)

なぜなら、OS(Debian)側でもcronによって無効なセッションファイルの削除を自動実行してるから(30分ごとに)
(gcよりはcronジョブで削除されることのほうが多いんじゃないかしら?)

A子

なんだ

だったら安心だね

B美

それじゃ「セッションタイムアウト」した場合の「自動ログアウト」処理を考えるわよ

Commandクラスを作って、それをcronで定期的に(例えば、1時間に1回)実行することで、「status」が「1(ログイン)」だったレコードを「0(ログアウト)」に戻すようにね

C菜

「statusが1」であるレコードを検索して、その結果をループで回して、「セッションIDを”sess_”の後ろに付けたファイル」が存在するかをチェックするです~

もしもファイルが存在するのならば何もしないで~
ファイルが無かったら(「セッションタイムアウト」なので)「ログアウト」処理を行えば良いんじゃないでしょうか~?

A子

んー?
あっ、そっか

それでうまくいきそうだね(多分)

C菜

それではさっそくコマンドクラスをbakeで作りましょう~

cd html/authapp[Enter]
bin/cake bake command cleanup[Enter]

名前は「cleanup」にしましたけど、良いですよね~?

A子

んじゃ、「src/Command」の中に生成された「CleanupCommand.php」を書き換えていくかぁ

以前コマンドクラスを作ったときのコードをコピペして修正するよ
(【CakePHP5応用編⑫】を参照)

private $Users;

public function initialize(): void
{
    parent::initialize();

    $this->Users = $this->fetchTable('Users');
}

これで「users」テーブルを使う準備はOKね

B美

execute」メソッドだけど、私のほうで作ってみたわ
(ちょっと難しいからね)

public function execute(Arguments $args, ConsoleIo $io)
{
    //処理がタイムアウトしないように
    set_time_limit(0);

    //ログイン中のユーザ一覧を取得
    $users = $this->Users->find()->where(['status' => 1]);

    //セッションファイルのディレクトリ
    $sessionPath = ini_get('session.save_path');

    foreach ($users as $user) {
        //セッションファイルの有無を調査
        $sessionFile = $sessionPath.'/sess_'.$user->session;
        $sessionFileExists = file_exists($sessionFile);

        //セッションファイルが存在しなければログアウト状態に変更
        if (!$sessionFileExists) {
            $this->Users->updateAll(
                ['status' => 0, 'session' => null],
                ['id' => $user->id]
            )
;
        }
    }

    return null;
}

A子

set_time_limit」とか「ini_get」とか「updateAll」って、初めて出てきたような?

それって何なのさ?

B美

そのくらい自分で調べなさいね

てか、それこそが経験値の獲得につながるんだから

A子

うへぇ、分かったよ
(ググってみる)

set_time_limit」は引数にゼロを渡すことで処理がタイムアウトしないようにするってやつだね
(言い換えれば、時間のかかる処理ってタイムアウトすることがあるみたい…)

ini_get」は「php.ini」の設定値を取得する関数みたい
(「ini_get('session.save_path')」では「/var/lib/php/sessions」という文字列が得られる…ってことだね)

最後の「updateAll」はレコード更新を行うCakePHPのメソッドで、最初の引数が更新内容を記述した連想配列、次の引数が条件を記述した連想配列ってこと
(つまり、さっきのコードだと、このユーザの「status」と「session」を更新する…って意味))

B美

よくできました

まぁ、「set_time_limit(0)」は多分いらないんだけどね(苦笑)
(念のため…って感じ)

C菜

テストしてみるです~

まずはログインしてから30分くらい放置しますね~
(ちなみに、B美部長のアカウントです~)


A子

んじゃ、セッションファイルがまだ存在している状況で、cleanupコマンドを実行してみるよ

bin/cake cleanup[Enter]

うわっ、エラーだ!
B美の書いたコードなのに何でよ?

B美

もう忘れたの?

キャッシュファイルのパーミッション

C菜

そうでした~

cd[Enter]
cd html/authapp/tmp/cache/models[Enter]
su[Enter](一時的にrootになって)
chmod 666 myapp_cake_model_default_users[Enter]
exit[Enter](一般ユーザに戻る)

のあと、もう一度

bin/cake cleanup[Enter]

を実行です~

A子

うん、大丈夫みたいだね

んじゃ、次はセッションファイルが削除されるのを待とう
(待ち時間がムダに思えるけど、自動的にセッションファイルが削除されることの確認も含めてね)

うーん、なかなか削除されない…(苦笑)
(結局、約50分ほどで削除されたよ)

C菜

それではcleanupコマンドを実行してみますね~

bin/cake cleanup[Enter]

すごいです~
ばっちり「users」テーブルが更新されましたよ~



B美

それじゃ、ユーザ管理画面でユーザの一覧表示を行う際、ログインまたはログアウトを表示するとともに、ログイン中なら「削除」リンクを表示しない

…ってのをやってみなさい

A子

簡単、簡単
こんな感じでどうかな?

テストでは、私(ako@friction-river.jp)とC菜(cina@friction-river.jp)の二人がログイン状態ってことにしてみたよ



C菜

私(cina@friction-river.jp)だけがログアウトすると、こうなります~

B美

ふむ、良い感じね

じゃあ、最後にcron設定まで終わらせておきましょうか
ちゃんと覚えてるわよね?

A子

ももも、もちろんだよ(汗)

C菜

この反応は忘れてますね~(苦笑)

crontab -e[Enter]

のあと

0 * * * * /home/bimi/html/authapp/bin/cake cleanup

ですよ~
(【コラム⑨】を参照)

A子

あぁ、これで毎時0分に実行…ってことか
(要するに、1時間おきに実行)

B美

こういう書き方もできるけどね

0 * * * * cd /home/bimi/html/authapp && bin/cake cleanup

まぁ、どっちでも構わないんだけど…

C菜

それでは、B美部長の方法で記述してみますね~

B美

ログアウトせずにブラウザを閉じることでセッションタイムアウトを発生させ、このcronジョブによって適切に処理されるかどうかを必ずテストしてみなさいね

そういうところ、面倒だからって手を抜かないように!

A子

へいへい(苦笑)