CakePHP 認証機能

12月 9, 2007 · Posted in CakePHP, PHP 

ログイン認証出来る機能がほしかったのだが標準で実装されているACLって認証機能じゃなかったのね orz
データベースなどのユーザーテーブル上のID/PWで認証できる機能を探していたところ、いろいろとモジュールが見つかり以下が参考になった。

とはいえ、個人で実装するにはいろいろと足りないところもあったりしたので、いいところを真似しつつ自作してみた。
それなりのレベルまで作れた感じな為公開してみることにした。不具合点などはご指摘下さい。
またご利用に際しては自己責任でお願いします。

使い方

コンポーネントとして動作。controllers/components にauth.phpとして配置。

まずはコンポーネントを利用するためにコントローラのはじめに以下のように設定

  var $components = array('Auth');

また、認証が必要なアクションを設定。
beforeFilter() メソッド中に認証が必要になるアクションを定義する方法を採用。

    function beforeFilter() {
        // 認証設定
        $this->Auth->requireAuth('userMenu', 'editUser');
        
        // 二重登録防止
        $this->Security->requireAuth('editUser');
    }

Securityコンポーネントと同様の方法で、認証が必要なアクションについて、requireAuth()の中にカンマ区切りでアクション名を羅列する方法としました。
検索して見つけたもろもろのモジュールでは、すべてに対して認証をしていたり、アクションの実行時に呼び出しを行ったりと面倒な方法をしているのが多かったのと、Securityコンポーネントと同じ方法で対応した方がわかりやすかったという理由で、こうしてみた。
Security->requireAuth()で二重投稿防止などをしている場合はその宣言より前に行う必要があります。

課題としては、無制限に認証をかける場合は allとか指定すると認証とか出来るといいのかもしれません。ま、今のところ必要ないから作ってないけどw

認証されているかをチェックする場合は、

  $this->Auth->checkAuth();

にてチェック。

ログアウトをする場合は、

  $this->Auth->logout();

とする感じで。

また、認証は ‘member’テーブルの ‘userid’ / ‘password’ を利用して行い、該当するユーザーが見つかった場合は ‘member’テーブル上のそのデータを認証ユーザーデータとして保存します。
実際は ‘Member’モデルで find()したユーザーデータが配列として保存されます。それらの値を取得する場合は、

  $value = $this->Auth->getAuthData('フィールド名');

member_name というフィールドの値を取りたい時は getAuthData(‘member_name’) で取得。

コンポーネント側の設定

$authFormName:
ま、authFormNameという命名が正しいかどうかは微妙です。
ログインフォームで $html->input(‘Login/userid’) などのように指定する際のモデル名(左では’Login’)を指定。

$authModelName:
認証で指定するモデル(テーブル)名。

$authLoginIdField:
ユーザーIDが格納されているフィールド名。 $authModelNameテーブルの $authLoginIdFieldがIDということになります。

$authPasswordField:
パスワードが格納されているフィールド名。 $authModelNameテーブルの $$authPasswordFieldがパスワードということになります。

$authLifeTime:
$authLifeTimeUnit:
上が認証有効期間、下がその単位。60分の場合は
$authLifeTime = 60;
$authLifeTimeUnit = ‘minutes’;
となる。strtotime() で比較するのでそこで認識できる単位を指定する。

$loginFormUrl:
ログインフォームURL。ログインが必要な場合はこのURLにリダイレクトされる。

$sessionName:
認証情報を保存するためのセッション名。ま、適当に。

ログインエラーとしては ID不正、パスワード不正、認証有効期間オーバーを用意しており、それらが
$this->Auth->getError() で取得出来るので、取得した内容をログインエラーとして表示できるんだけど、この辺がもうちょっとうまいこと実現できないかなーと思っているところ。
現状、たとえば loginForm() というアクションを用意して

    function loginForm() {
        $this->data[$this->Auth->authFormName] = $this->Auth->getLoginFormData();
        $this->set('errMsgs', $this->Auth->getError());
    }

のように errMsgsにエラー情報を渡してログイン画面を表示する必要があり、一つアクションに命令を各必要がある。
できればここは、書かずに処理したいんですよね。コンポーネント側だけで解決出来るといいかなーと。

書くべきところはこんな感じかしら?

ソース[auth.php]


class AuthComponent extends Object
{
    var $controller = true;
    var $components = array('RequestHandler', 'Session');
    
    /** 認証が必要なアクション名を格納する配列 */
    var $requireAuth = array();
    
    /** ログインフォームのフォーム名 */
    var $authFormName = 'Login';
    
    
    /** 認証で利用するモデル */
    var $authModelName = 'Member';
    var $authModel = null;
    var $authMember = null; 
    
    /** 認証モデルのログインID フィールド名 */
    var $authLoginIdField = 'login_id';

    /** 認証モデルのパスワード フィールド名 */
    var $authPasswordField = 'password';
    
    /** 認証有効期間 */
    var $authLifeTime = 60;
    /** 認証有効期間の単位 */
    var $authLifeTimeUnit = 'minutes';
    /** 認証有効時間(期間から算出) */
    var $authExpire;
    
    /** ログインフォームへのURL */
    var $loginFormUrl = '/members/loginForm';
    
    /** ログイン完了後にリダイレクトするURL */
    var $authLoginBackUrl = '';
    
    /** 認証で利用するセッション名 */
    var $sessionName = 'dragonquest';
    /**
     * セッションに引き渡し認証比較をするためのデータ
     *      array('Member' => array(),  ---> ログインしたユーザーの情報、authModelからfindして取得
     *            'Login'  => array(),  ---> ログインフォームでの ID/PWを作成するためのフォーム情報
     *            'errors'  => array(),  ---> ログインエラー情報
     *            'loginBackUrl' => String,   ---> 認証完了後に実行するアクション 
     *            'expire' => String,   ---> ログインの有効時間 strtotime() の形式で
     *            '' => '',
     *           )
     */
    var $authData = array();
    var $authErrors = array();
    var $authDataErrorSessName = 'errors';
    var $authLoginBackUrlSessName = 'loginBackUrl';
    var $authExpireSessName    = 'expire';
    
    /* 認証エラー関係 */
    var $AUTH_NORMAL        = 0;
    var $AUTH_IS_NOT_LOGIN  = 1;
    var $AUTH_LOGIN_ID_ERR  = 2;
    var $AUTH_PASSWORD_ERR  = 3;
    var $AUTH_OVER_LIFETIME = 4;
    var $errMsgs = array(0  => '',
                         1  => 'ログインして下さい。',
                         2  => 'ユーザーIDが正しくありません。',
                         3  => 'パスワードが正しくありません。',
                         4  => 'ログイン有効時間をすぎたためログアウトしました。再度ログインして下さい。'
                        );
    
    /** StartUp */
    function startup(&$controller) {
        $this->controller =& $controller;
        
        // 初期処理
        $this->_init();
        
        // 認証処理を行うアクションの場合は処理の開始
        if (in_array($controller->action, $this->requireAuth)) {
            $this->start();
        } else {
            $this->setLoginBackUrl();
        }
        
        // authDataをセッションに書き出す
        $this->Session->write($this->sessionName, $this->authData);
    }
    
    
    /**
     * 初期処理
     */
    function _init() {
        //
        // 利用するモデルの定義
        // 認証データが格納されているモデルを定義
        //
        loadModel($this->authModelName);
        $className = $this->authModelName;
        $this->authModel =& new $className();
        
        // セッションに認証データがある場合は $authDataに値を設定
        if ($this->Session->check($this->sessionName)) {
            $this->authData = $this->Session->read($this->sessionName);
        }
        
        // 別名参照の作成
        // $this->authData[xxxxx] と書くのを省略したいだけw
        $this->authErrors =& $this->authData[$this->authDataErrorSessName];
        $this->authExpire =& $this->authData[$this->authExpireSessName];
        $this->authLoginBackUrl =& $this->authData[$this->authLoginBackUrlSessName];
    }
    
    
    /**
     * 認証制限するための action名を設定
     * 
     * @params  array   引数で定義されたアクション名は認証制限をかける物とする
     */
    function requireAuth() {
        $this->requireAuth = func_get_args();
    }

    
    /**
     * 認証処理の開始
     */
    function start() {
        // 認証開始時にエラー情報破棄(ログインエラー時用にエラー情報をセッションで渡しているため)
        $this->authData[$this->authDataErrorSessName] = array();
        
        // 
        // ログインフォームから入った場合は
        // ログイン処理を実行
        //
        if ($this->_isLoginExecute()) {
            if(!$this->login($this->controller->data[$this->authFormName][$this->authLoginIdField],
                            $this->controller->data[$this->authFormName][$this->authPasswordField])) {
                // ログイン失敗時
                $this->_redirectLoginForm();
            } else {
                // 別名参照の作成
                // ログイン成功時はログイン実行によって取得した会員情報を authDataに渡す
                $this->authData[$this->authModelName] =& $this->authMember[$this->authModelName];
                // ログイン有効時間を更新
                $this->setExpire();
            }
        } else {
            // 別名参照の作成
            // ログイン画面を経由していない場合は authMember にセッションに持っていた会員情報データを渡す
            $this->authMember[$this->authModelName] =& $this->authData[$this->authModelName];
        }
        
        //
        // 未ログイン時はログイン画面を表示
        //
        if  (!$this->checkAuth()){
            // ログイン完了後戻るべき URLをセット
            $this->setLoginBackUrl();
            
            // ログインエラーをセット
            $this->setError($this->AUTH_IS_NOT_LOGIN);
            $this->authData[$this->authFormName] = array();
            
            // ログインフォームへリダイレクト
            $this->_redirectLoginForm();
        }
        
        //
        // ログインしていても有効時間を過ぎていれば
        // やっぱりログイン画面を表示
        //
        if (!$this->checkExpire()) {
            $this->setError($this->AUTH_OVER_LIFETIME);
            $this->authData[$this->authFormName] = array();
            $this->_redirectLoginForm();
        }
        
        //
        // 問題なければ、認証時刻を記録
        //
        $this->setExpire();
    }
    
    /**
     * ログイン処理
     * 
     * @param   String  $login_id
     * @param   String  $password
     * @return  Boolean True: ログイン成功 / False: ログイン失敗
     */
    function login($login_id, $password) {
        if (empty($login_id)) {
            $this->setError($this->AUTH_LOGIN_ID_ERR);
            return false;
        }
        
        //
        // ユーザーID / パスワードのチェック
        //
        $this->authMember =& $this->authModel->find(array($this->authLoginIdField => $login_id));
        if (empty($this->authMember)) {
            $this->setError($this->AUTH_LOGIN_ID_ERR);
            return false;
        }
        
        if ($this->authMember[$this->authModelName][$this->authPasswordField] != $password) {
            unset($this->authMember[$this->authModelName]);
            $this->authData[$this->authFormName][$this->authLoginIdField] = $login_id;
            $this->authData[$this->authFormName][$this->authPasswordField] = $password;
            
            // ログインエラーメッセージを表示してログイン画面を表示
            $this->setError($this->AUTH_PASSWORD_ERR);
            return false;
        }
        
        return true;
    }
    
    /**
     * ログアウト処理
     */
    function logout() {
        // セッションデータを削除する
        $this->Session->delete($this->sessionName);
    }
    
    
    /**
     * ログインされているかどうかのチェックを行う
     */
    function checkAuth() {
        return ($this->Session->check($this->sessionName) and !empty($this->authData[$this->authModelName]));
    }
    
    
    
    /**
     * ログイン有効時間をセットする
     * セットされた時間を過ぎるとログイン無効となる
     */
    function setExpire() {
        // authLifeTime (単位: authLifeTimeUnit) から有効時間を算出
        $this->authExpire = strtotime('+'.$this->authLifeTime.' '.$this->authLifeTimeUnit);
    }
    
    /**
     * ログイン時間が有効かどうかをチェックする
     * 
     * @return  boolean     True: 有効なログイン
     */
    function checkExpire() {
        // authExpireが現在時刻いないであれば有効とする
        return (strtotime('now') < $this->authExpire);
    }

    
    /**
     * ログインエラーをセットする
     * 
     * @param   Integer $errno  ログインエラーNo.
     */
    function setError($errno) {
        // ログインエラーNo. のメッセージも含めて
        // authErrors に対して格納
        $this->authErrors[$errno] = $this->errMsgs[$errno];
    }
    
    
    /**
     * ログインエラー情報を取得する
     * 
     * @return  array    array(errno => errmsg, errno => errmsg, ...)
     */
    function getError() {
        return $this->authErrors;
    }



    /**
     * ログインフォームから遷移したかどうか?
     * 
     * @return  boolean     true: ログイン画面からの遷移
     */
    function _isLoginExecute() {
        // authModelName を名称とした POSTデータがわたる時はログインフォームからの遷移とする
        return !empty($this->controller->data[$this->authFormName]);
    }


    /**
     * ログイン完了後に戻るべきURLへリダイレクト
     */
    function _redirectLoginBack() {
        $this->Session->write($this->sessionName, $this->authData);
        $login_back_url = !empty($this->authLoginBackUrl) ? $this->authLoginBackUrl : '/';
        $this->controller->redirect($login_back_url);
    }


    /**
     * ログイン画面へリダイレクトする
     * ログイン画面で表示するエラーメッセージ、入力フォームデータをセッションに格納し
     * リダイレクト
     */
    function _redirectLoginForm() {
        $this->Session->write($this->sessionName, $this->authData);
        $this->controller->redirect($this->loginFormUrl);
    }
    
    
    /*
     * Setter/Getter
     */
    function getAuthData($field = null) {
        if ($this->checkAuth()) {
            if ($field == null) {
                return $this->authData[$this->authModelName];
            } else {
                return $this->authData[$this->authModelName][$field];
            }
        } else {
            return false;
        }
    }
    function setAuthData($field, $value) {
        if ($this->checkAuth()) {
            $this->authData[$this->authModelName][$field] = $value;

            // authDataをセッションに書き出す
            $this->Session->write($this->sessionName, $this->authData);
        }
    }
    
    
    
    
    /**
     * フォームを中得する
     */
    function getLoginFormData() {
        if ($this->checkAuth()) {
            return $this->authData[$this->authFormName];
        } else {
            return false;
        }
    }
    
    
    /**
     * 実行中のURLを取得する
     * 実行したアクションに戻れるように
     * 
     * @return String  アクションURL
     */
    function getLoginBackUrl() {
        if ($this->checkAuth()) {
            return $this->authData[$this->authLoginBackUrlSessName];
        } else {
            return false;
        }
    }
    
    
    /**
     * 実行中のURLを取得する
     * 実行したアクションに戻れるように
     * 
     * @return String  アクションURL
     */
    function setLoginBackUrl() {
        if (getenv('REQUEST_URI') != $this->loginFormUrl) {
            // ログイン完了後戻るべき URLを現在のURLとして 
            $this->authLoginBackUrl = getenv('REQUEST_URI');
        }
    }
}

Comments

5 Responses to “CakePHP 認証機能”

  1. 匿名 on 3月 30th, 2008 21時38分33秒

    認証対象ページを表示させた場合、ログインページが表示されます。
    しかし、その後、別ページ経由や直接ログインページを表示した場合、認証エラー情報が消えていません。
    何かよい回避策はありますか?

  2. MJ on 3月 31st, 2008 22時17分17秒

    お試しいただきありがとうございます。
    ログイン画面への URLを直接たたいたときに、ログイン画面が表示されてしまうのは確認できましたが、こちらで試した限りではエラー情報は消えておりました。

    セッションに保存をしているのでそのあたりなのかもしれません。
    ログイン成功時などのタイミングでエラー情報破棄すればいいのかもしれないです。
    調べてみますね。
    (いまちょっとテンパッているので余裕でたら見ますね。すいません)

  3. MJ on 6月 10th, 2009 1時09分54秒

    一年以上間を空けといて何ですが
    同じ auth機能について Akelos用にコンポーネント化して組み込んだ際に連絡頂いた不具合を解消したので、こっちも修正を。

    $this->authLoginBackUrlの持ち方の問題でした。
    _redirectLoginBack()について修正しました。

    ちなみに未検証です(´・ω・`)

  4. deg on 2月 14th, 2011 17時03分46秒

    セッションを保持しているみたいなので、
    beforeFilterのエラーはく手前で、
    $this->Session->delete(‘Message.auth.message’);
    したら消えました。

    ちなみに、私は親コントローラに書き込んだ。

  5. MJ on 2月 14th, 2011 17時24分43秒

    コメントありがとうございます。
    参考にさせていただきます。

    今は、CakePHPを使わなくなり、フレームワークもなにを使うか迷走していますw

Leave a Reply