気絶するほど簡単な自動ログインの実装

注意

この実装はクッキーにユーザ名とパスワードを保持させていますが、パスワードを保持させるのは大変危険なので、実際のアプリケーションで動かす場合は時限つきAuthorizeトークンを発行してそれを保持させる実装にするなど、クッキー盗聴対応を必ずしましょう。

トークンを使う実装に修正しました。(16:25)

この実装は、クッキー盗聴対策のため、トークンを発行し、それをクッキーに保存します。
CakePHPのクッキーコンポーネントは賢く、Security.ciperSeedというキーを元に復号可能な暗号化をクッキーに対し施しているため、直に読めることはないのですが、それでも解析されたら丸見えになります。これを避けるため、パスワードを直接保存することがないようにしましょう。

ワンタイムトークンを使う実装に修正しました。(18:51)

いつも同じトークンを発行してしまうと、再生(リプレイ)攻撃の脆弱性が起こりうるためです。(thanks to @yohwaki)

準備

hack_pluginプラグインとして配置。

AuthコンポーネントにAppAuthをエイリアスマッピングしちゃいます。
<?php
class AppController extends Controller{
	$components = array('Auth', 'Hack.Alias' => array('Auth' => 'AppAuth'));
}
Userモデルにトークンを返すメソッドを作る

特定の条件で同じトークンを発行するメソッドを作っておきます。
トークンの発行・トークンからのユーザーの検索をするメソッドを作っておきます。
今回はtoken()というメソッド名とします。
※生成するトークンはランダムかつ期限付きにするようにしないと、再生攻撃による脆弱性の恐れがあります。

引数を渡すと、トークンとして扱われそれをもとにユーザーIDを返し、何も指定しないとトークンを発行・ユーザモデルに保存してそれを返すというものです。

実装

"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();
?>