CakePHP 3に標準で用意されているForm認証モジュールは、ユーザIDとパスワード以外の値を入力して認証することを考慮していません。
組織などのグループに所属するユーザを認証する際にグループIDを入力して認証するには、カスタム認証モジュールを利用することになります。
Form認証の基本
まず、標準のForm認証モジュールで認証する方法です。
基本のUsersテーブルだけで認証する場合は、次のようなコードを使用します。
Controller/AppController.php
public function initialize()
{
:
/*
* ユーザ認証機能を登録
*/
$this->loadComponent('Auth', [
'authenticate' => [
'Form' => [
'fields' => [
'username' => 'email',
'password' => 'password',
],
],
],
'loginAction' => [
'controller' => 'Users',
'action' => 'login',
],
:
]);
Template/Users/login.ctp
<h1><?= __('Sign In') ?></h1>
<?= $this->Form->create() ?>
<?= $this->Form->control('email') ?>
<?= $this->Form->control('password') ?>
<?= $this->Form->button(__('Sign In')) ?>
<?= $this->Form->end() ?>
この記述でUsersテーブルにあるemailカラムとpasswordカラムを使って、認証することができるようになります。
Form認証を拡張する
次に、以下のような2つのテーブルを用いて認証することを検討してみたいと思います。
create table `groups` (
`id` INT not null AUTO_INCREMENT
, `code` VARCHAR(16) not null
, `name` VARCHAR(32) not null
, `expiration` DATETIME not null
, `status` CHAR(1) not null
);
create unique index `groups_IX1`
on `groups`(`code`);
create table `users` (
`id` CHAR(36) not null
, `group_id` INT not null
, `code` VARCHAR(32) not null
, `name` VARCHAR(250) not null
, `password` VARCHAR(250) not null
, `status` CHAR(1) not null
);
create unique index `users_IX1`
on `users`(`group_id`,`code`);
〇〇グループに所属する□□さんを認証することを目論んでいます。
そのため、テンプレートは、
Template/Users/login.ctp
<h1><?= __('Sign In') ?></h1>
<?= $this->Form->create() ?>
<?= $this->Form->control('group') ?>
<?= $this->Form->control('code') ?>
<?= $this->Form->control('password') ?>
<?= $this->Form->button(__('Sign In')) ?>
<?= $this->Form->end() ?>
のようにします。
Authコンポーネントの登録は、
Controller/AppController.php
public function initialize()
:
/*
* ユーザ認証機能を登録
*/
$this->loadComponent('Auth', [
'authenticate' => [
'Myform' => [
'fields' => [
'username' => 'code',
'password' => 'password',
],
'finder' => 'auth',
'contain' => [
'Groups' => [
'fields' => [
'code' => 'group',
],
'conditions' => [
'Groups.status <>' => 'D',
'Groups.expiration >= NOW()'
],
]
],
],
],
'loginAction' => [
'controller' => 'Users',
'action' => 'login',
],
:
]);
のようにします。
標準の認証モジュールからはcontainの設定方法を拡張しています。
拡張に伴ってcontainの設定は、
'contain' => [
'<モデルのクラス名>' => [
'fields' => [
'<カラム名>' => '<フィールド名>',
...
],
'conditions' => [
'<検索条件>',
...
],
]
],
の形式で登録します。
次にこの設定に合わせ、Form認証モジュールを参考にしてカスタム認証モジュールを作成します。
Auth/MyformAuthenticate.php
<?php
namespace App\Auth;
use Cake\Http\Response;
use Cake\Http\ServerRequest;
use Cake\Auth\BaseAuthenticate;
use Cake\ORM\TableRegistry;
class MyformAuthenticate extends BaseAuthenticate
{
protected $_defaultConfig = [
'fields' => [
'username' => 'username',
'password' => 'password',
],
'userModel' => 'Users',
'scope' => [],
'finder' => 'all',
'contain' => null,
'passwordHasher' => 'Default',
];
protected function _checkFields(ServerRequest $request, array $fields)
{
foreach ([$fields['username'], $fields['password']] as $field) {
$value = $request->getData($field);
if (empty($value) || !is_string($value)) {
return false;
}
}
return true;
}
protected function _checkContainFields(ServerRequest $request, array $contains)
{
foreach ($contains as $contain) {
$fields = $contain['fields'];
foreach ($fields as $column => $field) {
$value = $request->getData($field);
if (empty($value) || !is_string($value)) {
return false;
}
}
}
return true;
}
protected function _query($username, array $params = null)
{
$config = $this->_config;
$table = TableRegistry::get($config['userModel']);
$options = [
'conditions' => [$table->aliasField($config['fields']['username']) => $username]
];
if (!empty($config['scope'])) {
$options['conditions'] = array_merge($options['conditions'], $config['scope']);
}
if (!empty($config['contain'])) {
if (is_array($config['contain'])) {
foreach ($config['contain'] as $key => $val) {
$conditions = [];
foreach ($val['fields'] as $column => $name) {
$conditions += [$key.'.'.$column => $params[$key][$column]];
}
if (!empty($val['conditions'])) {
$conditions = array_merge($val['conditions'], $conditions);
}
$options['contain'][] = $key;
$options['conditions'] = array_merge($options['conditions'], $conditions);
}
}
else {
$options['contain'] = $config['contain'];
}
}
$finder = $config['finder'];
if (is_array($finder)) {
$options += current($finder);
$finder = key($finder);
}
if (!isset($options['username'])) {
$options['username'] = $username;
}
return $table->find($finder, $options);
}
protected function _findUser($username, $password = null, array $params = null)
{
$query = $this->_query($username, $params);
$result = $query->first();
if (empty($result)) {
$hasher = $this->passwordHasher();
$hasher->hash((string)$password);
return false;
}
$passwordField = $this->_config['fields']['password'];
if ($password !== null) {
$hasher = $this->passwordHasher();
$hashedPassword = $result->get($passwordField);
if (!$hasher->check($password, $hashedPassword)) {
return false;
}
$this->_needsPasswordRehash = $hasher->needsRehash($hashedPassword);
$result->unsetProperty($passwordField);
}
$hidden = $result->getHidden();
if ($password === null && in_array($passwordField, $hidden)) {
$key = array_search($passwordField, $hidden);
unset($hidden[$key]);
$result->setHidden($hidden);
}
return $result->toArray();
}
public function authenticate(ServerRequest $request, Response $response)
{
$fields = $this->_config['fields'];
if (!$this->_checkFields($request, $fields)) {
return false;
}
$contains = $this->_config['contain'];
if (!$this->_checkContainFields($request, $contains)) {
return false;
}
$options = [];
foreach ($contains as $table => $val) {
$option = [];
foreach ($val['fields'] as $column => $name) {
$option[$column] = $request->getData($name);
}
$options[$table] = $option;
}
return $this->_findUser(
$request->getData($fields['username']),
$request->getData($fields['password']),
$options
);
}
}
また、'finder' => 'auth'を設定しましたので、
Model/Table/UsersTable.php
public function findAuth(\Cake\ORM\Query $query, array $options)
{
$query->where(['Users.status <>' => 'D']);
return $query;
}
のようにfindAuthを追加します。
以上で、複数のテーブルをつないで認証を行うことができるようになりました。