CakePHP3の認証[Auth]コンポーネントを用いてログイン機能を実装する
- 公開:
- 更新:
- カテゴリ: PHP CakePHP
- タグ: PHP,Auth,login,CakePHP,3.5,Component
今回はCakePHP3の認証コンポーネントを使ってログイン機能を実装します。
- アジェンダ
開発環境
今回の開発環境に関しては以下の通りです。
- Linux CentOS 7
- Apache 2.4
- PHP 7.1
- MySQL 5.7
- CakePHP 3.5
CakePHPのルートディレクトリを「cakephp/」とします。
尚、各ファイルの生成にはbakeコマンドを用いますので、実行可能ではない場合はbin/cakeに実行権限を付与しておいてください。
認証コンポーネント
大抵のPHPフレームワークには、簡単に認証機能(ログインやログアウト)を構築できる仕組みが備わっており、CakePHPにも「認証コンポーネント」という、認証に関する処理を扱うコンポーネントが用意されているので、それを使って素早く認証機能を構築していきます。
[Cookbook]認証
https://book.cakephp.org/3.0/ja/controllers/components/authentication.html
[Cookbook]シンプルな認証と認可のアプリケーション
https://book.cakephp.org/3.0/ja/tutorials-and-examples/blog-auth-example/auth.html
認証ロジックに必要なものの用意
認証コンポーネントについて解説する前に、前段として必要なファイルやデータの用意を行います。それらの準備は完了していて、認証についてだけ確認したい場合はこのセクションを飛ばしてください。
テーブル作成
まずは、ユーザを管理するusersテーブルを作成します。マイグレーションで作成していきます。
マイグレーションを有効化していない場合は以下を参考に有効化してください。
CakePHP3のマイグレーションでデータベースを構築する
マイグレーションファイルの作成
テーブル構築の為のマイグレーションファイルを作成します。CakePHPのルートディレクトリへ移動し、以下のbakeコマンドを叩きます。
# CakePHPのルートディレクトリへ移動する
cd /path/to/cakephp
# bakeコマンドでUserテーブルのマイグレーションファイルを生成する
bin/cake bake migration CreateUsers username:string password:string email:string:unique:EMAIL_INDEX role:integer[1]:indexType:indexRole last_login_at created modified
# 実行結果
[demo@localhost cakephp]# bin/cake bake migration CreateUsers username:string password:string email:string:unique:EMAIL_INDEX role:integer[1]:indexType:indexRole last_login_at created modified
Creating file /var/www/html/cakephp/config/Migrations/XXXX_CreateUsers.php
Wrote `/var/www/html/cakephp/config/Migrations/XXXX_CreateUsers.php`
cakephp/config/Migrations 配下に XXXX_CreateUsers.php が生成されます。
cakephp
├─ config
│ ├─ Migrations
│ │ ├─ XXXX_CreateUsers.php
- cakephp/config/Migrations/XXXX_CreateUsers.php
-
<?php
use Migrations\AbstractMigration;
class CreateUsers extends AbstractMigration
{
public function change()
{
$table = $this->table('users');
$table->addColumn('username', 'string', [
'default' => null,
'limit' => 255,
'null' => false,
]);
$table->addColumn('password', 'string', [
'default' => null,
'limit' => 255,
'null' => false,
]);
$table->addColumn('email', 'string', [
'default' => null,
'limit' => 255,
'null' => false,
]);
$table->addColumn('role', 'integer', [
'default' => null,
'limit' => 1,
'null' => false,
]);
$table->addColumn('last_login_at', 'datetime', [
'default' => null,
'null' => false,
]);
$table->addColumn('created', 'datetime', [
'default' => null,
'null' => false,
]);
$table->addColumn('modified', 'datetime', [
'default' => null,
'null' => false,
]);
$table->addIndex([
'email',
], [
'name' => 'EMAIL_INDEX',
'unique' => true,
]);
$table->addIndex([
'role',
], [
'name' => 'indexRole',
'unique' => false,
]);
$table->create();
}
}
マイグレーションファイルは変更せずにこのまま使用します。
マイグレーション実行
それではマイグレーションを実行してテーブルを作成します。CakePHPのルートディレクトリへ移動し、以下のbakeコマンドを叩きます。
# CakePHPのルートディレクトリへ移動する
cd /path/to/cakephp
# bakeコマンドでマイグレーションを実行する
bin/cake migrations migrate
# 実行結果
[demo@localhost cakephp]# bin/cake migrations migrate
using migration paths
- /var/www/html/cakephp/config/Migrations
using seed paths
- /var/www/html/cakephp/config/Seeds
using environment default
using adapter mysql
using database cakephp
== XXXX CreateUsers: migrating
== XXXX CreateUsers: migrated 0.5555s
All Done. Took 0.5572s
using migration paths
- /var/www/html/cakephp/config/Migrations
using seed paths
- /var/www/html/cakephp/config/Seeds
Writing dump file `/var/www/html/cakephp/config/Migrations/schema-dump-default.lock`...
Dump file `/var/www/html/cakephp/config/Migrations/schema-dump-default.lock` was successfully written
MySQLへログインしテーブルを確認します。
mysql> show tables;
+-------------------+
| Tables_in_cakephp |
+-------------------+
| phinxlog |
| users |
+-------------------+
mysql> show columns from users;
+---------------+--------------+------+-----+---------+----------------+
| Field | Type | Null | Key | Default | Extra |
+---------------+--------------+------+-----+---------+----------------+
| id | int(11) | NO | PRI | NULL | auto_increment |
| username | varchar(255) | NO | | NULL | |
| password | varchar(255) | NO | | NULL | |
| email | varchar(255) | NO | UNI | NULL | |
| role | int(1) | NO | MUL | NULL | |
| last_login_at | datetime | NO | | NULL | |
| created | datetime | NO | | NULL | |
| modified | datetime | NO | | NULL | |
+---------------+--------------+------+-----+---------+----------------+
usersテーブルが作成された事が確認できました。
モデル作成
次に、usersテーブルを扱うモデルを作成します。CakePHPのルートディレクトリへ移動し、以下のbakeコマンドを叩きます。
# CakePHPのルートディレクトリへ移動する
cd /path/to/cakephp
# bakeコマンドでusersテーブルのモデルを生成する
bin/cake bake model Users
# 実行結果
[demo@localhost cakephp]# bin/cake bake model Users
One moment while associations are detected.
Baking table class for Users...
Creating file /var/www/html/cakephp/src/Model/Table/UsersTable.php
Wrote `/var/www/html/cakephp/src/Model/Table/UsersTable.php`
Deleted `/var/www/html/cakephp/src/Model/Table/empty`
Baking entity class for User...
Creating file /var/www/html/cakephp/src/Model/Entity/User.php
Wrote `/var/www/html/cakephp/src/Model/Entity/User.php`
Deleted `/var/www/html/cakephp/src/Model/Entity/empty`
cakephp/src/Model/Table 配下に UsersTable.php そして、cakephp/src/Model/Entity 配下に User.php が生成されます。
cakephp
├─ src
│ ├─ Model
│ │ ├─ Entity
│ │ │ └─ User.php
│ │ └─ Table
│ │ └─ UsersTable.php
- cakephp/src/Model/Table/UsersTable.php
-
<?php
namespace App\Model\Table;
use Cake\ORM\Query;
use Cake\ORM\RulesChecker;
use Cake\ORM\Table;
use Cake\Validation\Validator;
/**
* Users Model
*
* @method \App\Model\Entity\User get($primaryKey, $options = [])
* @method \App\Model\Entity\User newEntity($data = null, array $options = [])
* @method \App\Model\Entity\User[] newEntities(array $data, array $options = [])
* @method \App\Model\Entity\User|bool save(\Cake\Datasource\EntityInterface $entity, $options = [])
* @method \App\Model\Entity\User patchEntity(\Cake\Datasource\EntityInterface $entity, array $data, array $options = [])
* @method \App\Model\Entity\User[] patchEntities($entities, array $data, array $options = [])
* @method \App\Model\Entity\User findOrCreate($search, callable $callback = null, $options = [])
*
* @mixin \Cake\ORM\Behavior\TimestampBehavior
*/
class UsersTable extends Table
{
/**
* Initialize method
*
* @param array $config The configuration for the Table.
* @return void
*/
public function initialize(array $config)
{
parent::initialize($config);
$this->setTable('users');
$this->setDisplayField('id');
$this->setPrimaryKey('id');
$this->addBehavior('Timestamp');
}
/**
* Default validation rules.
*
* @param \Cake\Validation\Validator $validator Validator instance.
* @return \Cake\Validation\Validator
*/
public function validationDefault(Validator $validator)
{
$validator
->integer('id')
->allowEmpty('id', 'create');
$validator
->scalar('username')
->maxLength('username', 255)
->requirePresence('username', 'create')
->notEmpty('username');
$validator
->email('email')
->requirePresence('email', 'create')
->notEmpty('email')
->add('email', 'unique', ['rule' => 'validateUnique', 'provider' => 'table']);
$validator
->scalar('password')
->maxLength('password', 255)
->requirePresence('password', 'create')
->notEmpty('password');
$validator
->integer('role')
->requirePresence('role', 'create')
->notEmpty('role');
$validator
->dateTime('last_login_at')
->requirePresence('last_login_at', 'create')
->notEmpty('last_login_at');
return $validator;
}
/**
* Returns a rules checker object that will be used for validating
* application integrity.
*
* @param \Cake\ORM\RulesChecker $rules The rules object to be modified.
* @return \Cake\ORM\RulesChecker
*/
public function buildRules(RulesChecker $rules)
{
$rules->add($rules->isUnique(['username']));
$rules->add($rules->isUnique(['email']));
return $rules;
}
} - cakephp/src/Model/Entity/User.php
-
<?php
namespace App\Model\Entity;
use Cake\ORM\Entity;
use Cake\Auth\DefaultPasswordHasher;
/**
* User Entity
*
* @property int $id
* @property string $username
* @property string $email
* @property string $password
* @property int $role
* @property \Cake\I18n\FrozenTime $last_login_at
* @property \Cake\I18n\FrozenTime $created
* @property \Cake\I18n\FrozenTime $modified
*/
class User extends Entity
{
/**
* Fields that can be mass assigned using newEntity() or patchEntity().
*
* Note that when '*' is set to true, this allows all unspecified fields to
* be mass assigned. For security purposes, it is advised to set '*' to false
* (or remove it), and explicitly make individual fields accessible as needed.
*
* @var array
*/
protected $_accessible = [
'username' => true,
'email' => true,
'password' => true,
'role' => true,
'last_login_at' => true,
'created' => true,
'modified' => true
];
/**
* Fields that are excluded from JSON versions of the entity.
*
* @var array
*/
protected $_hidden = [
'password'
];
}
コントローラ作成
続いて、コントローラを作成します。CakePHPのルートディレクトリへ移動し、以下のbakeコマンドを叩きます。
# CakePHPのルートディレクトリへ移動する
cd /path/to/cakephp
# bakeコマンドでusersコントローラを生成する
bin/cake bake controller Users
# 実行結果
[demo@localhost cakephp]# bin/cake bake controller Users
Baking controller class for Users...
Creating file /var/www/html/cakephp/src/Controller/UsersController.php
Wrote `/var/www/html/cakephp/src/Controller/UsersController.php`
Bake is detecting possible fixtures...
cakephp/src/Controller 配下に UsersController.php が生成されます。
cakephp
├─ src
│ ├─ Controller
│ │ ├─ UsersController.php
- cakephp/src/Controller/UsersController.php
-
<?php
namespace App\Controller;
use App\Controller\AppController;
/**
* Users Controller
*
* @property \App\Model\Table\UsersTable $Users
*
* @method \App\Model\Entity\User[]|\Cake\Datasource\ResultSetInterface paginate($object = null, array $settings = [])
*/
class UsersController extends AppController
{
/**
* Index method
*
* @return \Cake\Http\Response|void
*/
public function index()
{
$users = $this->paginate($this->Users);
$this->set(compact('users'));
}
/**
* View method
*
* @param string|null $id User id.
* @return \Cake\Http\Response|void
* @throws \Cake\Datasource\Exception\RecordNotFoundException When record not found.
*/
public function view($id = null)
{
$user = $this->Users->get($id, [
'contain' => []
]);
$this->set('user', $user);
}
/**
* Add method
*
* @return \Cake\Http\Response|null Redirects on successful add, renders view otherwise.
*/
public function add()
{
$user = $this->Users->newEntity();
if ($this->request->is('post')) {
$user = $this->Users->patchEntity($user, $this->request->getData());
if ($this->Users->save($user)) {
$this->Flash->success(__('The user has been saved.'));
return $this->redirect(['action' => 'index']);
}
$this->Flash->error(__('The user could not be saved. Please, try again.'));
}
$this->set(compact('user'));
}
/**
* Edit method
*
* @param string|null $id User id.
* @return \Cake\Http\Response|null Redirects on successful edit, renders view otherwise.
* @throws \Cake\Network\Exception\NotFoundException When record not found.
*/
public function edit($id = null)
{
$user = $this->Users->get($id, [
'contain' => []
]);
if ($this->request->is(['patch', 'post', 'put'])) {
$user = $this->Users->patchEntity($user, $this->request->getData());
if ($this->Users->save($user)) {
$this->Flash->success(__('The user has been saved.'));
return $this->redirect(['action' => 'index']);
}
$this->Flash->error(__('The user could not be saved. Please, try again.'));
}
$this->set(compact('user'));
}
/**
* Delete method
*
* @param string|null $id User id.
* @return \Cake\Http\Response|null Redirects to index.
* @throws \Cake\Datasource\Exception\RecordNotFoundException When record not found.
*/
public function delete($id = null)
{
$this->request->allowMethod(['post', 'delete']);
$user = $this->Users->get($id);
if ($this->Users->delete($user)) {
$this->Flash->success(__('The user has been deleted.'));
} else {
$this->Flash->error(__('The user could not be deleted. Please, try again.'));
}
return $this->redirect(['action' => 'index']);
}
}
テンプレート作成
最後にテンプレートを生成します。CakePHPのルートディレクトリへ移動し、以下のbakeコマンドを叩きます。
# CakePHPのルートディレクトリへ移動する
cd /path/to/cakephp
# bakeコマンドでテンプレートを生成する
bin/cake bake template Users
# 実行結果
[demo@localhost cakephp]# bin/cake bake template Users
Baking `index` view template file...
Creating file /var/www/html/cakephp/src/Template/Users/index.ctp
Wrote `/var/www/html/cakephp/src/Template/Users/index.ctp`
Baking `view` view template file...
Creating file /var/www/html/cakephp/src/Template/Users/view.ctp
Wrote `/var/www/html/cakephp/src/Template/Users/view.ctp`
Baking `add` view template file...
Creating file /var/www/html/cakephp/src/Template/Users/add.ctp
Wrote `/var/www/html/cakephp/src/Template/Users/add.ctp`
Baking `edit` view template file...
Creating file /var/www/html/cakephp/src/Template/Users/edit.ctp
Wrote `/var/www/html/cakephp/src/Template/Users/edit.ctp`
cakephp/src/Template/Users 配下にそれぞれ
index.ctp
view.ctp
add.ctp
edit.ctp
が生成されます。(今回これらは変更しないので初期ソースは割愛します)
認証ロジックの実装
お待たせしました。準備も終わったのでここから認証機能を実装していきます。
認証コンポーネントの追加
まずは、認証コンポーネントを追加します。CakePHPでは基底クラスに使用するコンポーネントを定義する事で認証を行えるようになります。
- src/Controller/AppController.php
-
<?php
namespace App\Controller;
use Cake\Controller\Controller;
use Cake\Event\Event;
class AppController extends Controller
{
public function initialize()
{
parent::initialize();
$this->loadComponent('RequestHandler');
$this->loadComponent('Flash');
}
}
以下のように記述します。
<?php
namespace App\Controller;
use Cake\Controller\Controller;
use Cake\Event\Event; // ← beforeFilter()メソッドを実装する場合は追加
class AppController extends Controller
{
public function initialize()
{
parent::initialize();
$this->loadComponent('RequestHandler');
$this->loadComponent('Flash');
$this->loadComponent('Auth', [
'loginAction' => [
'controller' => 'Users',
'action' => 'login'
],
'loginRedirect' => [
'controller' => 'Users',
'action' => 'index'
],
'logoutRedirect' => [
'controller' => 'Users',
'action' => 'login',
'home'
],
'authenticate' => [
'Form' => [
'fields' => ['username' => 'email', 'password' => 'pass']
]
],
]);
}
// 認証を通さないアクションがある場合のみ
public function beforeFilter(Event $event)
{
//$this->Auth->allow(['add']);
}
}
上記、追加記述した部分を解説します。
$this->loadComponent('Auth', [
'loginAction' => [
'controller' => 'Users',
'action' => 'login'
],
'loginRedirect' => [
'controller' => 'Users',
'action' => 'index'
],
'logoutRedirect' => [
'controller' => 'Users',
'action' => 'login',
'home'
],
'authenticate' => [
'Form' => [
'fields' => ['username' => 'email', 'password' => 'pass']
]
],
]);
Flashコンポーネントの後にAuthコンポーネント追加の記述を行っています。
loginActionプロパティでログイン画面のコントローラーとアクションを指定しています。
loginRedirectプロパティで、ログイン後のリダイレクト先を指定しています。ここではユーザの一覧ページである「users/index」にリダイレクトを設定しました。
logoutRedirectプロパティでは、ログアウト後のリダイレクト先を指定しています。ここではログイン画面である「users/login」にリダイレクトしています。
authenticateプロパティでは、認証に使用するカラムを指定しています。ここでは、ユーザーにemailカラム、パスワードにpassカラムを使う事を指定しています。
// 認証を通さないアクションがある場合のみ
public function beforeFilter(Event $event)
{
$this->Auth->allow(['add']);
}
このbeforeFilter()メソッドですが、認証を通さないアクションがある場合に、ここで指定する事で、認証をスルー事が出来ます。
例えば、
「ユーザの追加や編集は認証をつけたいけど、一覧などは誰でも見られるようにしたい。」
などと言った場合には、ここにユーザ一覧ページのアクションを追加する事で、ログインしなくても閲覧できるようになったりなどの使い方が出来ます。
そして、beforeFilter()メソッドを使う場合は、Eventクラスを忘れずにuseします。
use Cake\Event\Event; // ← 追加
コントローラ実装
次に、コントローラ側の実装を行います。既存のソースコードへの変更はありませんが、3つほどアクションを追加します。追加文のみを以下に記します。
namespace App\Controller;
use App\Controller\AppController;
use Cake\Event\Event; // ← 追加
class UsersController extends AppController
{
/**
* 認証スルー設定
* @param Event $event
* @return \Cake\Http\Response|null|void
*/
public function beforeFilter(Event $event)
{
parent::beforeFilter($event);
$this->Auth->allow(['add', 'hoge']);
}
/**
* ログイン
* @return \Cake\Http\Response|null
*/
public function login()
{
if ($this->request->is('post')) {
$user = $this->Auth->identify();
if ($user) {
$this->Auth->setUser($user);
return $this->redirect($this->Auth->redirectUrl());
}
$this->Flash->error(__('ユーザ名もしくはパスワードが間違っています'));
}
}
/**
* ログアウト
* @return \Cake\Http\Response|null
*/
public function logout()
{
return $this->redirect($this->Auth->logout());
}
上から解説します。
/**
* 認証スルー設定
* @param Event $event
* @return \Cake\Http\Response|null|void
*/
public function beforeFilter(Event $event)
{
parent::beforeFilter($event);
$this->Auth->allow(['add', 'hoge', 'ext']);
}
基底クラスでも出てきたbeforeFilter()メソッドです。認証を通さないアクションをここでも指定できます。
もしAppControllerでもスルー設定を行っていた場合には、parent::beforeFilter()メソッドを記述しましょう。行っていない場合には記述の必要はありません。スルー設定は必ず必要なわけではないので適宜、必要な場合に実装してください。
use Cake\Event\Event; // ← 追加
そして先ほどと同じように、beforeFilter()メソッドを使う場合は、Eventクラスを忘れずにuseします。
/**
* ログイン
* @return \Cake\Http\Response|null
*/
public function login()
{
if ($this->request->is('post')) {
$user = $this->Auth->identify();
if ($user) {
$this->Auth->setUser($user);
return $this->redirect($this->Auth->redirectUrl());
}
$this->Flash->error(__('ユーザ名もしくはパスワードが間違っています'));
}
}
ログインアクションです。postリクエストであれば認証コンポーネントを用いて認証を行い、その結果を返しています。もちろん、getリクエストの場合はそのままビューを返すのでログイン画面が表示されます。
/**
* ログアウト
* @return \Cake\Http\Response|null
*/
public function logout()
{
return $this->redirect($this->Auth->logout());
}
ログアウトアクションです。認証コンポーネントにてログアウト処理を行い、指定のリダイレクト先へ送ります。
以上でコントローラ側の実装は完了です。
パスワードのハッシュ化
次に、ユーザ追加を行った際の、パスワードのハッシュ化を設定します。ちなみにこれを行わないと、ハッシュ化されずに登録されるので、忘れずに実装します。
ハッシュ化の設定はEntityファイルで行うので、UserEntityに以下記述します。
- src/Model/Entity/User.php
-
namespace App\Model\Entity;
use Cake\ORM\Entity;
use Cake\Auth\DefaultPasswordHasher; // ← 追加
class User extends Entity
{
protected $_accessible = [
'username' => true,
'email' => true,
'password' => true,
'role' => true,
'last_login_at' => true,
'created' => true,
'modified' => true
];
protected $_hidden = [
'password'
];
protected function _setPassword($password)
{
if (strlen($password) > 0) {
return (new DefaultPasswordHasher)->hash($password);
}
}
}
ハッシュ化を行っているのは_setPassword()メソッドになります。
// パスワードのハッシュ化を行う
protected function _setPassword($password)
{
if (strlen($password) > 0) {
return (new DefaultPasswordHasher)->hash($password);
}
}
そして、ハッシュ化の為にDefaultPasswordHasherクラスが必要なので忘れずにuseします。
use Cake\Auth\DefaultPasswordHasher; // ← 追加
ハッシュ化の設定はこれで完了です。
セッションをクッキーでも保持する
セッションをクッキーでも保存する場合は、APPの設定ファイルに記述します。
- cakephp/config/app.php
-
'Session' => [
'defaults' => 'php',
'cookie' => 'your_app_cookie_name',
'timeout' => 43200,
'cookieTimeout' => 43200,
'autoRegenerate' => false,
'checkAgent' => false,
'ini' => array(
'session.cookie_lifetime' => 43200,
'session.gc_maxlifetime' => 43200,
'session.gc_divisor' => 43200,
'session.cookie_httponly' => true // XSS対策
),
'use_cookies' => 1,
'cookie_lifetime' => 43200,
],
ログインのテンプレート作成
最後に、ログイン画面のテンプレートを簡単に作成します。
- src/Template/Users/login.ctp
-
<div class="users form">
<?= $this->Flash->render() ?>
<?= $this->Form->create() ?>
<fieldset>
<legend><?= __('ユーザ名とパスワードを入力してください') ?></legend>
<?= $this->Form->control('username') ?>
<?= $this->Form->control('password') ?>
</fieldset>
<?= $this->Form->button(__('Login')); ?>
<?= $this->Form->end() ?>
</div>
これで一通りの作業は完了しました。
動作確認
それでは実際にブラウザからアクセスし、動作確認を行います。
まずはユーザを登録します。
http://YOUR-DOMAIN/users/add
にアクセスします。
コントローラ側で認証をスルーさせているので、アクセスが可能な状態です。ここでユーザを登録します。
ユーザ登録が出来たら実際にログインを行ってみます。
http://YOUR-DOMAIN/users/login
にアクセスします。
ログインに成功すると、ユーザ一覧のページが表示されます。
http://YOUR-DOMAIN/users
ちなみにログインに失敗すると以下のようなメッセージが表示されます。
また、ログインしていない状態でユーザ一覧へのURLへアクセスしてみると、エラーメッセージと共にログイン画面へリダイレクトされる事も確認できると思います。
最後に、ログアウトを行う場合は
http://YOUR-DOMAIN/users/logout
にアクセスします。ログアウトされると、ログイン画面に遷移します。
無事にログイン機能が構築できました。
まとめ
作業は以上で完了です。認証機能というのはあれこれ管理する必要があり地味に手間がかかるものですが、こうしてコンポーネントとして用意されている事で、素早く構築が可能になります。
CakePHPでログイン機能を構築するならの認証コンポーネントはかなり役に立つので、是非試してみてください。