Skip to content

Instantly share code, notes, and snippets.

@yano3nora
Last active December 18, 2018 06:57
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save yano3nora/210fc9f1a620b052034d0fec469d33e0 to your computer and use it in GitHub Desktop.
Save yano3nora/210fc9f1a620b052034d0fec469d33e0 to your computer and use it in GitHub Desktop.
[cakephp: AuthComponent] AuthComponent on CakePHP3. #php #cakephp

OVERVIEW

FallbackPasswordHasher

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');

Sample Code

開発時パスワード抜きでサクっとログインしたい

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());
}

AuthComponent 設定例

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;
  }


}

AuthComponent からコールするカスタム Finder

$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;
  }


}

AuthComponent でカスタム認証ハンドラの利用を設定

// AdminAppController.php

$this->Auth->config('authenticate', [
  'AdminForm' => [  // 上記で作成したカスタム認証ハンドラ App\Auth\AdminForm を指定
    'userModel' => 'Users',
    'fields'    => [
      'username' => 'username',
      'password' => 'password',
    ],
    'finder' => 'admin',  // Finder 条件はこっちで拡張すべき
  ],
]);

シンプルな認可ハンドラを ControllerAuthorize コールバックで実装する

ControllerAuthorize では、コントローラーのコールバックで認可チェックを処理することができます。 非常にシンプルな認可を行う場合や、認可を行うのにモデルとコンポーネントを合わせて利用する必要がある場合、 しかしカスタム認可オブジェクトを作成したくない場合に、これは理想的です。 - ControllerAuthorize の利用

認可ハンドラをガリガリ設定していくの結構しんどい ... 複雑な「認証 → 権限による認可」の設定が必要なアプリケーションでなければ、大体はこのコールバックでなんとかなるのではっていうやつ。

Auth の認証領域を分けずに個別の認可ハンドラを設定

正直名前空間切ったりしてる時点で 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 パターンを手前に書くほうが書きやすい
  }

}

認証 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 にしていても ...

  1. /users/edit とかにいる状態で認証セッションが切れて ...
  2. リロードすると /users/login ( loginAction ) へ飛ばされて ...
  3. ログインすると /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());
  }
}

?redirect= なクエリがつくとだめなんだけど ...

ログインフォームを $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= とかくっつかなくなる -->

別 sessionKey の Auth に代理ログイン

<?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]);
  }

}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment