- シンプルな認証と認可のアプリケーション - book.cakephp.org
- 認証 - book.cakephp.org
- 認可 - book.cakephp.org
- 認可ハンドラーの設定
- カスタム認可オブジェクトの作成
- 認可を使用しない
- AuthComponent はデフォルトで認可ハンドラを OFF ってます。
- 公開する ( 認可をスルーさせる ) アクションの作成
loginAction
をAuthComponent::allow()
に加えちゃダメ。- 上記の理由から CakePHP は
loginAction
内でログアウトさせるような実装を想定していないぽい。
- 認可が必要なアクションの作成
- ControllerAuthorize の利用 ( isAuthorized() )
- AuthComponent 設定オプション - book.cakephp.org
- 認証が必要なアクションのテスト - book.cakephp.org
CakePHP 2.x ではデフォルトで SHA1 アルゴリズムによるハッシュ化を行っているが CakePHP3 は DefaultPasswordHasher
クラスの内部で BCRYPT アルゴリズムによるハッシュ化がデフォルト。このため 2 → 3 移行時や両者を共存させるときにハッシュアルゴリズムの違いが生まれユーザログイン認証ができなくなる。これを解決するのが FallbackPasswordHasher
ちゃん。
CakePHP3 cookbook - 3.0移行ガイド
CakePHP3 cookbook - ハッシュ化アルゴリズムの変更
CakePHP3を触ってみました 〜SHA1(シャア)という名のオールドタイプ〜
// AuthComponent での設定時
$this->loadComponent('Auth', [
'authenticate' => [
'Form' => [
'passwordHasher' => [
'className' => 'Fallback', // FallbackPasswordHasher を使います宣言
'hashers' => [
'Weak', // WeakPasswordHasher ( 内部的に SHA1 で照合 ) を最初に試す
'Default' // ダメだったら DefaultPasswordHasher ( 内部的に BCRYPT で照合 ) を試す
]
]
]
]
]);
// 単体で使う時
$hasher = new FallbackPasswordHasher(['hashers' => ['Weak', 'Default']]); // 上記と同じように試すクラスを複数指定
$password = $hasher->hash('test1234');
public function login()
{
// こんな感じのこと書いておけばパスワード適当でログインできると思うよ
$this->loadModel('Users');
$username = $this->request->data('username');
$user = $this->Users->findByUsername($username)->first();
$this->Auth->setUser($user);
return $this->redirect($this->Auth->redirectUrl());
}
Admin 領域用の共通コントローラを想定。
// AdminAppController.php
namespace App\Controller\Admin;
use App\Controller\AppController;
use Cake\Event\Event;
class AdminAppController extends AppController
{
/**
* Event: initialize.
* Load AuthComponent.
* @param void
* @return void
*/
public function initialize() {
parent::initialize();
$this->loadComponent('Auth');
}
/**
* Event: beforeFilter.
* Set auth configurations.
* @param Event $event
* @return void
*/
public function beforeFilter(Event $event) {
parent::beforeFilter($event);
$this->viewBuilder()->setLayout('admin');
$this->Auth->config('loginAction', [
'controller' => 'users',
'action' => 'login',
]);
$this->Auth->config('loginRedirect', [
'controller' => 'users',
'action' => 'dashboard',
]);
// $this->Auth->config('unauthorizedRedirect', false); // デフォルトではリファラか '/' へ | false だと 403 を吐く
$this->Auth->config('authenticate', [
// 'Basic' => ['userModel' => 'Members'], // Basic 認証用のハンドラ、API とかで利用
// 'Digest' => [ // Digest 認証用のハンドラ、同じく API とかで利用
// 'fields' => ['username' => 'username', 'password' => 'digest_hash'],
// 'userModel' => 'Users'
// ],
'Form' => [ // 一般的なフォーム ( セッション ) 認証ハンドラ
'userModel' => 'Users',
'fields' => [
'username' => 'username',
'password' => 'password',
],
'finder' => 'admin', // $this->Auth->identify() 時の finder を指定
// ( この場合 UsersTable::findAdmin() がコールされる )
],
]);
$this->Auth->config('storage', [
'className' => 'Session',
'key' => 'Auth.Admin', // 認証ロールが複数ある場合はここで認証データの保存領域を分ける
]);
$this->Auth->config('authError', MSG_ERROR_LOGIN);
$this->Auth->config('authorize', ['Controller']); // isAuthorized() など認可ハンドラ利用箇所宣言
$this->Auth->allow(['logout', 'restore']); // 認可をスルーさせるアクションを設定 ( 'loginAction' は除く )
}
/**
* Auth: Is authorized.
* ControllerAuthorize callback.
* @param $user
* @return bool
*/
public function isAuthorized($user=null) {
if (!empty($user)) {
$this->set('loggedUser', $user);
return true;
}
return false;
}
}
$this->Auth->config('authenticate')
で 'finder' を指定した場合のロジックを Model\Table クラスで管理できる。
// UsersTable.php
/**
* Finder: admin.
* @param Query $Query
* @param array $options
* @return Query
*/
public function findAdmin(Query $Query, array $options) {
// username と password で where するロジックは AuthComponent が勝手に追加してくれるぽい
return $Query->where(['Users.authority' => AUTHORITY_ADMIN]);
}
認証ハンドラをカスタムして
$this->Auth->identify()
が返す認証オブジェクトをこねる。
カスタム認証オブジェクトに挑戦してみる - CakePHP3で遊ぶ
// AdminFormAuthenticate.php
namespace App\Auth;
use Cake\Auth\BaseAuthenticate;
use Cake\Http\ServerRequest;
use Cake\Http\Response;
class AdminFormAuthenticate extends BaseAuthenticate
{
/**
* Custom authenticate for 'AdminForm'.
* Return extended user object when call AuthComponent::identify().
*
* @param ServerRequest
* @param Response
* @return User
*/
public function authenticate(ServerRequest $request, Response $response)
{
$username = $request->data('email');
$password = $request->data('password');
if (!$user = $this->_findUser($username, $password)) return false;
if ($user['id'] != $this->_query($username)->first()->id) return false;
$user = $this->_query($username)->first(); // To return User entity.
$user->logged_at = \DateTimeImmutable(); // Extends entity via login.
return $user;
}
}
// AdminAppController.php
$this->Auth->config('authenticate', [
'AdminForm' => [ // 上記で作成したカスタム認証ハンドラ App\Auth\AdminForm を指定
'userModel' => 'Users',
'fields' => [
'username' => 'username',
'password' => 'password',
],
'finder' => 'admin', // Finder 条件はこっちで拡張すべき
],
]);
ControllerAuthorize では、コントローラーのコールバックで認可チェックを処理することができます。 非常にシンプルな認可を行う場合や、認可を行うのにモデルとコンポーネントを合わせて利用する必要がある場合、 しかしカスタム認可オブジェクトを作成したくない場合に、これは理想的です。 - ControllerAuthorize の利用
認可ハンドラをガリガリ設定していくの結構しんどい ... 複雑な「認証 → 権限による認可」の設定が必要なアプリケーションでなければ、大体はこのコールバックでなんとかなるのではっていうやつ。
正直名前空間切ったりしてる時点で Auth の認証領域 ( Session key ) を分けた方が楽なんだけどこういう手もあるよってことで。
// AppController.php
namespace App\Controller;
use App\Controller;
class AppController extends Controller
{
public function initialize() {
parent::initialize();
$this->loadComponent('Auth');
$this->Auth->config('authorize', ['Controller']); // 認可ハンドラの利用領域を宣言
}
/**
* Auth: Is authorized.
* ControllerAuthorize callback.
* @param array|null $user - 認証オブジェクトを引数にとる
* @return bool
*/
public function isAuthorized($user=null) {
if ($this->request->getParam('prefix') == 'public') {
return true; // true を返せば「認可あり」扱い
}
if (!$this->request->getParam('prefix')) {
if (!empty($user)) {
$this->set('loggedUser', $user);
return true;
}
}
if ($this->request->getParam('prefix') == 'admin') {
if (!empty($user['role']) && $user['role'] == 'admin') {
$this->set('loggedUser', $user);
return true;
}
}
$this->Auth->config('authError', '権限がありません');
return false; // false を返せば「認可なし」扱いとなり Auth の設定に従いリダイレクト
// ガード節よりデフォルトで false で落として OK パターンを手前に書くほうが書きやすい
}
}
ログイン時はかならずコントローラで云々 ... とかは beforeFilter
かますよりこっちのほうが便利。
// UserAppController.php などで ...
public function isAuthorized($user=null) {
if (!empty($user)) {
$this->set('loggedUser', $user); // 認証 OK 時はかならずビューに $loggedUser をセットするとかね
return true;
}
return false;
}
このコントローラは「ユーザの権限レベル」や「アプリケーションの状態」で認可を振り分けたいんだよおおっていう時のやつ。親クラスが AuthComponent::isAuthorized()
をコールしている場合はちゃんとオーバライドすること。
namespace App\Controller\Admin;
use App\Controller\AdminAppController;
class UsersController extends AdminAppController
{
/**
* Auth: Is authorized.
* ControllerAuthorize callback.
* @param User|null $user
* @return bool
*/
public function isAuthorized($user=null)
{
try {
if (!parent::isAuthorized($user)) {
throw new \Exception('まずログインしてから出直してこい');
}
if (strtolower($this->request->action) == 'delete') {
if (!$user->has_privileged) {
throw new \Exception('削除は特権持ちの Admin じゃないとダメなの');
}
}
return true;
} catch (\Exception $e) {
$this->Auth->config('authError', $e->getMessage());
return false;
}
}
}
CakePHP3 ログイン後のURLを固定する - qiita.com
AuthComponent > 設定オプション - book.cakephp.org
AuthComponent は「認証領域へ未承認状態でのアクセス」を unauthorizedRedirect
先 ( loginRedirect
やリファラや /
) へぶっ飛ばす。このとき Auth.redirect
セッションが保存され、 loginRedirect
はこのセッションを保持しているとき無視される。よって loginRedirect
を /users/index
にしていても ...
/users/edit
とかにいる状態で認証セッションが切れて ...- リロードすると
/users/login
(loginAction
) へ飛ばされて ... - ログインすると
/users/index
ではなく/users/edit
にリダイレクト
... のような挙動になる。WEB サイト / サービスの場合は有用だが業務アプリではこれを無効化して「リダイレクト先が必ず loginRedirect
になるように」固定したいケースもある。以下コードで Auth.redirect
セッションを殺してあげればよい。
public function login() {
if ($this->request->is('post')) {
if (!$user = $this->Auth->identify()) {
$this->Flash->error('Failed to login.');
return $this->redirect($this->Auth->logout());
}
$this->Auth->setUser($user);
$this->request->session()->delete('Auth.redirect'); // Restrict dynamic redirect.
return $this->redirect($this->Auth->redirectUrl());
}
}
ログインフォームを $this->Form->create()
でオプションなし状態で作成していると、レンダリング時に <form action="/users/login?redirect=%2users%2edit">
のように動的に QueryString が付与されてしまう。上記実装に加えてオプションで固定してあげればよい。
<?php echo $this->Form->create(null, [
'url' => [
'controller' => 'users',
'action' => 'login',
],
]) ?>
<!-- action="/users/login" で固定されて ?redirect= とかくっつかなくなる -->
<?php
class UsersController extends AppController
{
/**
* Assign as proxy login
* @param int $user_id
*/
public function assign(int $user_id) {
$this->Roles = TableRegistry::get('Roles');
$assignUser = $this->Users->get($user_id, ['contain' => ['Roles']]);
$this->Auth->__set('sessionKey', 'Auth.'.ucfirst($assignUser->role->name));
$this->Auth->setUser($assignUser);
return $this->redirect(TOPPAGES[$assignUser->role->id]);
}
}