zz log

zaininnari Blog

Auth コンポーネントのパスワードハッシュ化回避

Auth コンポーネントの使用時に、
[/users/add] や [/users/edit/]
で、パスワードを入力させて、バリデーションをしたいとき、
アクションの段階では、既にパスワードがハッシュ化されているため、
バリデーションがやりづらい問題を回避します。


結論は、
Auth コンポーネント
「標準の AuthComponent::password 以外のハッシュ関数を使用する機能」を使って、
「ハッシュ化しないハッシュ関数」を作るという方法です。

環境

  • php : 5.3.2
  • cakephp : 1.3.2
    • 確認はしていませんが、API は 1.2系でもあったので、動くと思います。
  • database : mysql

参考

テーブルの作成

  • mysql のサンプルです。
--
-- テーブルの構造 `users`
--

CREATE TABLE IF NOT EXISTS `users` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `created` datetime DEFAULT NULL,
  `modified` datetime DEFAULT NULL,
  `username` varchar(200) COLLATE utf8_unicode_ci NOT NULL,
  `password` varchar(200) COLLATE utf8_unicode_ci NOT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB  DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci AUTO_INCREMENT=36 ;

Auth コンポーネントのおさらい

なんぞこれ?

概要を掴むには、このエントリが分かりやすいです。

なにをやっているの?

Auth コンポーネントは、
デフォルトの設定は、以下の様になっています。

<?php
// 認証に使うモデル
$this->Auth->userModel = 'User';
// ユーザー名 と パスワードのフィールド名
$this->Auth->fields    = array('username' => 'username', 'password' => 'password');

コントローラーの「add」や「edit」のアクションを実行する前の段階(beforeFilterの後)で、
$this->data['User']['password']
($this->data[$this->Auth->getModel()->alias][$this->Auth->fields['password']])
をハッシュ化します。

ただ、デフォルトでは、ハッシュ化を止める方法はありません。

また、バリデートをする際には、
モデル内では、$this->data[$model->alias] のパスが同じく使用されるため、
ハッシュ化したものをチェックすることになってしまいます。

どう回避するのか

モデルや password フィールドを退避する、など様々回避方法は考えられますが、
そこは、cakephp 、丁度よい機能があります。
AuthComponent::hashPasswords のソースです。

<?php
	function hashPasswords($data) {
// 標準の AuthComponent::password (Security::hash のラッパ)以外のハッシュ関数を使用する機能
// $this->authenticate がオブジェクトで、($this->authenticate のデフォルトは 「null」)
// そのオブジェクトが、'hashPasswords' というメソッドを持っていれば、そちらを実行して結果を返す。
		if (is_object($this->authenticate) && method_exists($this->authenticate, 'hashPasswords')) {
			return $this->authenticate->hashPasswords($data);
		}

		if (is_array($data)) {
			$model =& $this->getModel();

			if(isset($data[$model->alias])) {
				if (isset($data[$model->alias][$this->fields['username']]) && isset($data[$model->alias][$this->fields['password']])) {
					$data[$model->alias][$this->fields['password']] = $this->password($data[$model->alias][$this->fields['password']]);
				}
			}
		}
		return $data;
	}

つまり、
標準の AuthComponent::password 以外のハッシュ関数を使用する機能を使って、
「何もしないハッシュ関数」を作るという方法です。

ソース

add アクションのみ解説しています。
view は、記載していませんが、
bake で焼いたもので、変更を加えず利用できます。

<?php

// [app/models/user.php]
class User extends AppModel
{
//	var $validate = array( 略 )

// 「何もしない」ハッシュ関数
// 何も手を加えず、返すだけ。これだけ。
	function hashPasswords($data)
	{
		return $data;
	}
}

// [app/app_controller.php]
// Auth コンポーネントを読み込みます。
// ログイン状態での作業が多いという想定で、ここに記述していますが、この辺はお好みで。
class AppController extends Controller
{

	var $components = array(
		'Security',
		'Auth',
		'Session',
		//{{{!debug
		'DebugKit.Toolbar',
		//}}}!debug
	);
	
	function beforeFilter()
	{
// 共通のログイン不要なアクション
		$this->Auth->allow('index', 'view');
	}

}

// [app/controllers/users_controller.php]
class UsersController extends AppController
{

// ここでは、「ユーザーの一覧や個々のページは、ログインを必要とする」ということにしているので、
// parent::beforeFilter() は記述していません。
	function beforeFilter()
	{
// ログインしていない一般ユーザー登録を可能にするので、
// 「add」を許可します。
		$this->Auth->allow('add');
		
// 'add' と 'edit' のアクションについては、ハッシュ化させないため、場合分けをしています。
		// no hash passowrd action
		if (in_array($this->params['action'], array('add', 'edit'), true)) {
// 移譲するハッシュ関数を定義したモデルをセットします。
			$this->Auth->authenticate = ClassRegistry::init('User');
		}
	}

	function add()
	{
// $this->Auth->authenticate を設定しない(デフォルトの「null」)場合は
// この時点で、既に「password」はハッシュ化されています。
// 「何もしない」ハッシュ関数で、入力されたままの「password」を入手できます。
		if (!empty($this->data)) {
// モデルの $this->data などを初期化します。
// 手動でバリデートを行う前に、バリデートをするデータをモデルに伝えます。
			$this->User->create($this->data);
// $this->data (トークン値などは略)
//   → array(
//       'User' => array(
//         'username' => '<入力したユーザー名>',
//         'password' => '<入力した平パスワード>'
//       )
//     )
		
// 手動でバリデートを行います。
// バリデートに成功したならば、ハッシュ化をして保存を試みます。
//   手動で行わない場合は、
//   User::beforeSave で、コントローラーの
//   $this->data[$this->Auth->getModel()->alias][$this->Auth->fields['password']]
//   に当たる部分を Security::hash($password, null, true) で上書きことで対応できますが、
//   モデル内で、「$this->Auth->fields['password']」に当たる部分を動的に取得する方法が困難です。
//   保存する値ですので、「決め打ち」はできます。
			if ($this->User->validates()) {
// Set クラスで使用するパスを作ります。
// Auth コンポーネントのデフォルトの設定のため、
// モデルは、「User」。パスワードフィールドは、「password」です。
// $path 
//   → 'User.password'
					$path = $this->Auth->getModel()->alias . '.' . $this->Auth->fields['password'];
// 分けて解説します。
// $this->data (トークン値などは略)
//   → array(
//       'User' => array(
//         'username' => '<入力したユーザー名>',
//         'password' => '<入力した平パスワード>'
//       )
//     )
//
// $this->data から パスに相当する値を取得します。
// $this->data['User']['password'] と同じです。
// Set::extract($this->data, $path) 
//   → Set::extract($this->data, 'User.password')
//   → <入力した平パスワード>
//
// パスワードをハッシュ化します。
// $this->Auth->password(Set::extract($this->data, $path)))
//   → $this->Auth->password('<入力した平パスワード>')
//   → Security::hash('<入力した平パスワード>', null, true)
//   → <ハッシュ化されたパスワード>
//
// ハッシュ化したパスワードを上書きしています。
// $this->data['User']['password'] = $this->Auth->password($this->data['User']['password'])
// と同じです。
// $this->data の パス('User.password') に データ('<ハッシュ化されたパスワード>') を挿入します。
// Set::insert($this->data, $path, $this->Auth->password(Set::extract($this->data, $path)))
//   → Set::insert($this->data, 'User.password', '<ハッシュ化されたパスワード>')
//   → array(
//       'User' => array(
//         'username' => '<入力したユーザー名>',
//         'password' => '<ハッシュ化されたパスワード>'
//       )
//     )
					$this->data = Set::insert($this->data, $path, $this->Auth->password(Set::extract($this->data, $path)));
// 保存をします。
// モデル側に既にある $this->data の $this->data['User']['password'] はハッシュ化されていないため、
// 第一引数に、ハッシュ化したパスワードを含んだデータを渡すことで、
// ハッシュ化したパスワードを保存できます。
// 既にバリデート済みのため、第二引数に、「false」を与えることで、バリデートをせず、保存を試みます。
				if ($this->User->save($this->data, false)) {
					$this->Session->setFlash(__('The User has been saved', true));
					$this->redirect(array('action' => 'index'));
				}
			}
			$this->Session->setFlash(__('The User could not be saved. Please, try again.', true));
		}
}

変更履歴-2010-06-11
  • 「$this->User->create($this->data);」が抜けていたのを修正
    • 上記に加え、解説コメントを追加