カテゴリー
cakephp cakephp2 PHP

Cakephp2.xでのBlackHoleまとめ

CakePHPでSecurityコンポーネントを使っていると、時折発生するBlackHole。
発生時はエラーログを確認するしか詳細な原因がわからないのですが、多くの場合は以下のどれかに該当するのではないでしょうか。

  • CSRFチェックのトークンがUseOnceなのにフォーム送信後にブラウザの「戻る」とか押してからフォームを再送した。
  • フォームを生成してから30分過ぎた。(トークンのデフォルト有効期限が30分)
  • JavaScriptでHidden項目を書き換えた。
  • JavaScriptで入力項目を追加したり取り去ったりした。

ブラウザの戻るを想定する場合はトークンを複数回使えるように設定しなければいけません。(ただし、セキュリティー的には若干低下します)

複数回使いまわせるようにするにはControllerのbeforeFilterなどで以下のようにcsrfUseOnceをfalseにします。

$this->Security->csrfUseOnce = false;

また、トークンのデフォルト有効期限が短すぎると判断される場合はbeforFilterで

$this->Security->csrfExpires = '+1 hour';

とするか、または$componentsの設定で以下のように設定すると良いでしょう。

public $components = array(
    'Security' => array(
        'csrfExpires' => '+1 hour'
    )
);

そもそもCSRF対策不要だよ!ってアプリケーションでは、

public $components = array(
    'Security' => array(
        'csrfCheck' => false
    )
);

として、CSRF対策機能をばっさり無くすことも可能ですが、セキュリティー的に問題が残るため、多くの場合はおすすめできません。

あと、JavaScriptでフォームの中身を変更すると、ことごとくBlackHoleに吸い込まれる事がありますが、Securityコンポーネントを有効にするとデフォルトでついてくる機能「FormTamperingPrevention」のおかげです。

Cakeがフォームを生成した時にフォームの内容をまるごとハッシュ化しており、フォーム送信時にフォームの内容に差異が生じていると、フォームを不正に改変されたとみなしてBlackHoleに吸い込んでくれる機能です。

ただし、JavaScriptでHiddenに値をセットしたり、Ajaxを使う場合に項目を動的に追加したりなんかしてもBlackHoleに吸い込んでくれるので、そのような場合は以下の何れかの対策が必要です。

FormTamperingPreventionをまるっと無効にする場合はbeforeFilterで以下のように指定します。

$this->Security->validatePost = false;

また、特定のフィールドだけFormTamperingPreventionの対象外としたい場合(JavaScriptで値をセットするhidden項目など)は、ViewのFormHelperを用いて以下のように指定することで回避できます。

$this->Form->unlockFields('Model.field_name');

 

BlackHoleに吸い込まれた時の処理

BlackHoleに吸い込まれた場合、デフォルトではDebugモードにもよりますが、プロダクションモードでは真っ白なページが表示され、404エラーが返されてしまいます。
では実際に何らかの問題が生じてBlackHoleに吸い込まれるべくして吸い込まれた場合、どのようにハンドリングすればスマートでしょうか。

ここはひとまずCakePHPのCookBookにのっとってblackholeメソッドを用意し、そちらに処理を任せるようにしましょう。

public function beforeFilter() {
    $this->Security->blackHoleCallback = 'blackhole';
}

public function blackhole($type) {
    // handle errors.
}

上記はCookBookからの引用になります。またblackholeメソッドのパラメータ$typeは以下の値をとるようですが、内容はCookBookのほぼ直訳になります。

  • ‘auth’ フォームバリデーションエラーまたはコントローラ/アクションの不一致によるエラー。
  • ‘csrf’ CSRFエラー
  • ‘get’ HTTPメソッド制限違反
  • ‘post’ HTTPメソッド制限違反
  • ‘put’ HTTPメソッド制限違反
  • ‘delete’ HTTPメソッド制限違反
  • ‘secure’ SSLメソッド制限違反

なので、blackholeメソッド側で$typeを参照し、エラー内容に沿ったメッセージを表示してあげると良さそうです。

カテゴリー
cakephp

Cakephp2のAuthコンポーネントで複数ログイン機能を制御

CakePHP2による開発で、何種類かのログイン機能を一つのウェブサービスに実装したい場合のお話し。

たとえば管理者用機能と一般ユーザーでのログイン状態の切り分けや、一つのサイト内で別のサービスを立ち上げて2種類以上のユーザーを抱えるような場合、aclによる制御も一つの方法ですが、そこまで厳密にユーザー権限を管理する必要は無いケースも多々あるはず。

単純にユーザーテーブルとloginメソッド実装したコントローラーを複数用意して、「ログイン出来たー!」と思ったらログイン情報が錯綜してしまい、あっちの機能でログインしたら、こっちの機能のログイン状態がおかしくなった...。
という場合の対処方法。

簡単な話で、各コントローラごとにログインを管理するグループに合わせてAuthComponentのセッションキーを変えればOK。

たとえば管理者機能(Adminグループ)のログインと、一般ユーザー(Userグループ)でのログイン機能の2種類がある場合、Authコンポーネントで用いるユーザー用テーブルを2種類用意する。

#管理者ユーザーテーブル
CREATE TABLE `admins` (
  `id` int(11) unsigned NOT NULL AUTO_INCREMENT,
  `name` varchar(45) NOT NULL,
  `password` varchar(45) DEFAULT NULL,
  `created` datetime DEFAULT NULL,
  `modified` datetime DEFAULT NULL,
  PRIMARY KEY (`id`),
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

#一般ユーザーテーブル
CREATE TABLE `users` (
  `id` int(11) unsigned NOT NULL AUTO_INCREMENT,
  `name` varchar(45) NOT NULL,
  `password` varchar(45) DEFAULT NULL,
  `created` datetime DEFAULT NULL,
  `modified` datetime DEFAULT NULL,
  PRIMARY KEY (`id`),
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

当然対応するAdminModelとUserModelは作成するとして、コントローラも2種類(管理者用機能と一般ユーザー用機能)を用意する必要がある。

各コントローラでAuthコンポーネントを設定する時に、

  • ログインに使用するモデルをグループに合わせて指定
  • beforeFilterで各グループごとに個別のsessionKeyを指定

これだけで、狙ったユーザーグループを用いたログイン機能が実現できる。

//AdminsController
public $components = array(
	'Auth' => array(
		'authenticate' => array(
			'Form' => array(
				'fields' => array(
					'username' => 'name',
					'password' => 'password',
				),
				'userModel' => 'Admin',
			),
		),
	),
);
public function beforeFilter() {
	parent::beforeFilter();
	AuthComponent::$sessionKey = 'Auth.admins';
}
//UsersController
public $components = array(
	'Auth' => array(
		'authenticate' => array(
			'Form' => array(
				'fields' => array(
					'username' => 'name',
					'password' => 'password',
				),
				'userModel' => 'User',
			),
		),
	),
);
public function beforeFilter() {
	parent::beforeFilter();
	AuthComponent::$sessionKey = 'Auth.users';
}

こんな感じで設定しておけば、あとは通常のCakePHPアプリのようにloginアクションやふつうのアクションを定義していけば、2種類のログイン情報が錯綜することなく処理されます。

コントローラを増やす場合は、各コントローラで上記の設定を行う必要がある点を忘れないように注意!