気絶するほど簡単な自動ログインの実装
注意
この実装はクッキーにユーザ名とパスワードを保持させていますが、パスワードを保持させるのは大変危険なので、実際のアプリケーションで動かす場合は時限つきAuthorizeトークンを発行してそれを保持させる実装にするなど、クッキー盗聴対応を必ずしましょう。
トークンを使う実装に修正しました。(16:25)
この実装は、クッキー盗聴対策のため、トークンを発行し、それをクッキーに保存します。
CakePHPのクッキーコンポーネントは賢く、Security.ciperSeedというキーを元に復号可能な暗号化をクッキーに対し施しているため、直に読めることはないのですが、それでも解析されたら丸見えになります。これを避けるため、パスワードを直接保存することがないようにしましょう。
参考
- 発注者のためのWebシステム/Webアプリケーションセキュリティ要件書 | 株式会社トライコーダ (thanks to @suzuki)
- 解答:まちがった自動ログイン処理 (thanks to @utatyaku)
準備
hack_pluginをプラグインとして配置。
実装
"remenberme"といったキーのクッキーをもっていたら、自動ログインを試みます。
明示的にログアウトするときはこのクッキーを削除します。
UsersControler
<?php class UsersController extends AppController { var $name = 'Users'; var $components = array('Cookie'); function login(){ if ($this->Auth->user()) { // redirect to somewhere } else { if ($token = $this->Cookie->read('rememberme')) { $user_id = $this->User->token($token); if ($user_id) { if ($this->Auth->login($user_id)) { // AuthComponent::redirect() は自動リダイレクトのURLを返す $this->redirect($this->Auth->redirect(), null, true); } } else { $this->Cookie->delete('rememberme'); } } } } // 明示的にログアウト function logout(){ if ($this->Cookie->check('rememberme')) { $this->Cookie->delete('rememberme'); } $this->redirect($this->Auth->logout()); } }
Authコンポーネントを継承したAppAuthコンポーネントを作ります。
<?php // App/controllers/components/app_auth.php App::import('Component', 'Auth'); class AppAuthComponent extends AuthComponent{ // クッキーコンポーネントを追加 function __construct(){ $this->components[] = 'Cookie'; parent::__construct(); } // コントローラへの参照を保持 function initialize(&$controller, $settings = array()){ parent::initialize($controller, $settings); $this->Controller =& $controller; } function login($data = null){ $result = parent::login($data); // ログイン成功 if($result){ if(!empty($this->Controller->data['User']['rememberme'])){ $token = $this->getModel()->token(); // 2週間自動ログインのためのクッキーを保存 $this->Cookie->write('rememberme', $token, true, '+2 weeks'); } } return $result; } }
補足
ちなみに、Hackプラグインを使わないでUsersController::login()でこのような処理を実装しようとすると、Authコンポーネントのstartup()メソッドをトレースしなくてはならず、非常に汚くなります。
#修正前に比べると気絶するほど簡単・・・か?ってほど長くなりましたが、ご愛嬌ということで :D
おまけ
Userモデルの例
bakeryのソースを参考にしています。
- ワンタイムトークンを使う方式
- auto_loginsという別テーブルを使う
- カラムはid, user_id, expires, token
<?php class User extends AppModel { var $hasMany = array( 'AutoLogin', ); function token($token = null) { if ($token !== null) { $conditions = array( 'AutoLogin.token' => $token, 'AutoLogin.expires > '=> date('Y-m-d H:i:s'), ); return $this->AutoLogin->field('user_id', $conditions); } if($this->id){ $data = array(mt_rand(), mt_rand() => mt_rand()); $token = Security::hash(serialize($data), null, true); $expires = '+2 weeks'; $data = array( 'AutoLogin' => array( 'user_id' => $this->id, 'token' => $token, 'expires' => date('Y-m-d H:i:s', strtotime($expires)), ) ); $this->AutoLogin->create($data); $this->AutoLogin->save(); return $token; } return null; } }
ユーザモデルのカラムとしてトークン、トークンの存在期間を設けるのも一つの手です。(副作用として、複数のブラウザ・PCから自動ログインすることができなくなります)
login.ctp
<?php echo $this->Form->create('User', array('legend' => __('Login',true))); $fields = array( 'username' => array('label' => __('User name', true)), 'password' => array('label' => __('Password',true)), 'rememberme' => array('type' => 'checkbox', array('label' => __('Remember me',true))), ); echo $this->Form->inputs($fields, array('legend' => __('Login',true))); echo $this->Form->submit(__('Login', true)); echo $this->Form->end(); ?>