Qiitaで書くのは初めてなので不届きなどありましたらお許し下さい。
目的
ウェブアプリケーションを作っていると色々な制限を実装しなければならないことが多いと思います。
その中でログインしているユーザーができることを許可/拒否する権限を実装することがあると思います。
例えば特定のユーザーはこの機能を使うことができるけど、それ以外のユーザーは使えないようにするとか、ユーザーごとに設定するのは管理が大変だからロールに設定したいとかです。
自分は10年以上Javaでウェブアプリケーションを作っておりSpringではSpringSecurityを使って同様な機能の実装ができます。
Laravelではlaravel-permissionというパッケージを使うことで比較的簡単に実装することができます。
実際に簡単なアプリケーションを作って見たいと思います。
動作(開発)環境
- PHP 7.1
- Laravel 5.5
- PostgreSQL 10.0
サンプルアプリケーションの概要
サンプルアプリケーションでは、ユーザーはロールに属し、ロール毎にパーミッションを設定し利用できるコンテンツを制限してみます。
ロールは以下の通りです。
- 社長(owner)
- 店長(manager)
- 店員(staff)
コンテンツは以下の通りです。
- 社員の情報(users)
- 社員の評価(scores)
権限マトリックスは以下の通りです。
コンテンツ | 店員 | 店長 | 社長 |
---|---|---|---|
社員情報 | R | R/W | R/W |
社員の評価 | R | R/W | |
R = 読み取り、W = 書き込み |
ユーザー一覧は以下の通りです。
構成をシンプルにするためユーザー名 = ロール名にしました。
ユーザー | ログインID | パスワード |
---|---|---|
社長 | owner@test | owner |
店長 | manager@test | manager |
店員 | staff@test | staff |
アプリケーションを実行する準備
一から作るのは時間がかかるのでサンプルアプリケーションが動作するDocker環境をDocker-Composeで準備しました。
Docker-Composeが使用できる方
レポジトリをクローンしてもらうだけで大丈夫です。
# サンプルアプリケーションをクローンする
git clone https://github.com/beeete2/qiita-laravel-permission.git
# イメージをpullする
cd qiita-laravel-permission
docker-compose pull
Vagrantを使用できる方
ゲストOSのIPは192.168.33.51になりますのでIPが衝突する場合、その他カスタマイズが必要な場合はVagrantfile
ファイルを修正してください。
AnsibleでDockerとDocker-Compose等をインストールします。
# クローンしてインスタンスを起動する
git clone https://github.com/beeete2/qiita-laravel-permission.git
cd qiita-laravel-permission
vagrant up
# sshでゲストOSにログインする
ssh 192.168.33.51
cd /vagrant
# イメージをpullする
docker-compose pull
CentOS7を使用できる方
Ansibleなどをインストールしますので、使い捨ての環境を想定しています。
またすべてrootユーザーで作業してます。
# 必要なパッケージをインストールする
yum install epel-release
yum install git ansible
# サンプルアプリケーションをクローンする
git clone https://github.com/beeete2/qiita-laravel-permission.git
cd qiita-laravel-permission
# AnsibleでDocker、Docker-Composeをインストールする
ansible-playbook -i localhost, -c local playbook.yml --extra-vars "skip_user_in_group=Yes"
# コンテナをpullする
docker-compose pull
アプリケーションを見てみる
laravel-permissionの説明をする前に、アプリケーションを見てみたいと思います。
各イメージをビルドしてレポジトリディレクトリにいることを想定しています。
アプリケーションを起動する
docker-compose run web composer update
cat src/.env.example > src/.env
docker-compose run web php artisan key:generate
# docker-composeでdbコンテナとwebコンテナを起動する
docker-compose up -d
# マイグレーションする
docker-compose exec web php artisan migrate --seed
# オプション(実行する必要はない)
# scmemaspyでデータベースのスキーマー情報を出力する(build/schemaspy以下に出力される)
chmod 777 build/schemaspy # ザルですがお許しください。
docker-compose run scmemaspy
ブラウザで見てみる。
ブラウザで見てみます。
http://Docker-ComposeホストのIP/
Laravelのホーム画面が表示されると思いますので社長ロールでログインするため右上のLogin
をクリックします。
するとログイン画面が表示されると思いますのでE-Mail Addressにowner@test
、Passwordにowner
と入力しログインします。
するとメニューが表示されます。
次に社員情報をクリックします。
すると社員情報の一覧が表示されます。
社員情報の一覧の変更ボタンですがユーザーにwrite users
パーミッションがある場合表示しておりlaravel-permission
の機能を使って制御しています。
一旦ログアウトして社員ロールでログインします。
次はE-Mail Addressにstaff@test
、Passwordにstaff
と入力しログインします。
同じくメニューから社員情報をクリックすると社員情報の一覧が表示されますがAction欄に変更ボタンが表示されていないと思います。
laravel-permission
ではViewだけではなくルートでも制限を行えます。
社員ロールでログインしたまま
http:/Docker-ComposeホストのIP/users/3
にアクセスします。するとエラーページ(ステータスコード403)が表示されたと思います。
また社員ロールには社員の評価の権限がないのでメニューの社員の評価をクリックしてもエラーページが表示されます。
サンプルアプリケーションの説明
サンプルアプリケーションを見ながらlaravel-permission
を簡単に説明していきたいと思います。
使用するテーブルの概要
laravel-permission
は主に以下のテーブルを使います。
テーブル名 | 説明 |
---|---|
users | ユーザーのアカウント |
roles | ロールを管理するテーブル。今回の例だと社長など。 |
permissions | パーミッションを管理するテーブル。今回の例だと社員の情報を読めるなど。 |
role_has_permissions | ロールに設定されたパーミッション |
model_has_permissions | ユーザーに設定されたパーミッション |
model_has_roles | ユーザーに設定されたロール |
ちなみにテーブル名はconfig/permission.php
に記載されています。
プロジェクトにテーブル名の命名規則等ありあわせる必要が有る場合は、このファイルを修正することで変更可能です。
ロールとパーミッションの設定
ロールとパーミッションの設定を行います。
サンプルアプリケーションではSeederを使って作成しています。
以下は抜粋です。
パーミッションの設定
パーミッションは文字列になります。
サンプルではread users
のようにアクション
+ スペース + コンテンツ
というルールでパーミッションを設定しました。
public function run()
{
$permissions = [
'read users', // 社員情報を読める
'write users', // 社員情報を変更できる
'read scores', // 社員の評価を読める
'write scores', // 社員の評価を変更できる
];
foreach ($permissions as $permission) {
Permission::create(['name' => $permission]);
}
}
ロールの設定
public function run()
{
$roles = [
'owner', // 社長
'manager', // 店長
'staff', // 店員
];
foreach ($roles as $role) {
Role::create(['name' => $role]);
}
}
ロールにパーミッションを割り当てる
次にロールにパーミッションを割り当ててみます。
これもSeederを使って作成しています。
public function run()
{
// ownerロールのパーミッション設定
$permissions = [
'read users', // 社員情報を読める
'write users', // 社員情報を変更できる
'read scores', // 社員の評価を読める
'write scores', // 社員の評価を変更できる
];
$role = Role::findByName('owner');
$role->givePermissionTo($permissions);
// managerロールのパーミッション設定
$permissions = [
'read users', // 社員情報を読める
'write users', // 社員情報を変更できる
'read scores', // 社員の評価を読める
];
$role = Role::findByName('manager');
$role->givePermissionTo($permissions);
// staffロールのパーミッション設定
$permissions = [
'read users', // 社員情報を読める
];
$role = Role::findByName('staff');
$role->givePermissionTo($permissions);
}
ユーザーを作成しロールを割り当てる
最後にユーザーを作成しロールを割り当てます。
これもSeederを使って作成しています。
public function run()
{
// 社長(owner)ユーザーを作成する
$user = User::create([
'name' => '社長',
'email' => 'owner@localhost',
'password' => Hash::make('owner'),
]);
$user->assignRole('owner');
// 店長(manager)ユーザーを作成する
$user = User::create([
'name' => '店長',
'email' => 'manager@localhost',
'password' => Hash::make('manager'),
]);
$user->assignRole('manager');
// 店員(staff)ユーザーを作成する
$user = User::create([
'name' => '店員',
'email' => 'staff@localhost',
'password' => Hash::make('staff'),
]);
$user->assignRole('staff');
}
パーミッションに従ったコンテンツの制御
基本的にlaravel-permissionのREADMEに詳しく書いてありますが、ロールとパーミッションに従ったコンテンツの制御は以下のように行います。
Bladeの制御
Bladeでパーミッションがある場合は特定のメニューを表示したいということがあると思います。
その場合はblade.phpに以下のように書きます。
@can('read users')
// read usersパーミッションが割り当てられる場合実行される
@endcan
ロールの場合は以下のように書きます。
@role('owner')
// オーナーロールの場合実行される
@endrole
ミドルウェアの制御
この手のパーミッション制御を行うと、例えばあるパス(Route)には特定のパーミッションを持っているときにアクセスできるということをよく実装すると思います。これをミドルウェアを使って制御することができます。
最初にlaravel-permissionの制御ができるようにミドルウェアを追加します。
protected $routeMiddleware = [
// 省略
'role' => \Spatie\Permission\Middlewares\RoleMiddleware::class,
'permission' => \Spatie\Permission\Middlewares\PermissionMiddleware::class,
];
これでRouteでパーミッションとロールの制御が行えるようになりました。
例えば、特定のパスに対してread users
を持つユーザーだけアクセスできるようにしたいという場合は以下のように書きます。
Route::group(['middleware' => ['permission:read users']], function () {
// 処理を記載する
});
ロールで制御することも可能です。
以下のように書くとowner
ロールの場合だけアクセスすることができます。
Route::group(['middleware' => ['role:owner']], function () {
// 処理を記載する
});
今回のサンプル・アプリケーションでは社員情報の場合usersにはread users
パーミッション、ユーザーを変更するためのusers/1にはwrite users
が必要になります。
サンプル・アプリケーションでは以下のように実装しました。
Route::prefix('/users')->middleware(['permission:read users'])->group(function () {
Route::get('index', 'UsersController@index')->name('users.index');
Route::middleware(['permission:write users'])->group(function () {
Route::get('{user}', 'UsersController@update')->name('users.update');
});
});
結びに
こんな感じで実装できます。もちろん自分で実装することもできると思いますがシンプルな権限制御だったらlaravel-permission
である程度実装できるのではないかと思います。
その他
Userモデルでlaravel-permission
の機能を使うためにHasRolesトレイトを使用しています。
大体の処理はHasRolesトレイトに書いてあります。
class User extends Authenticatable
{
use Notifiable;
use HasRoles; // laravel-permission
}
Role名を取得したい
ユーザーのRole名は以下のように取得することができます。
$user->getRoleNames();
ロールを再設定したい
現在設定されているロールを削除して、設定し直したい場合は以下のようにします。
$user->syncRoles('owner');
ユーザーに設定されているPermissionを取得したい
// ユーザーに設定されているPermission(Direct Permission)
$user->getDirectPermissions();
// ユーザーが所属するロールに設定されているPermission
$user->getPermissionsViaRoles();
// ユーザーに与えられているパーミッション(Direct PermissionとRole Permissionをマージ)
$user->getAllPermissions();
Guardごとにロールとパーミッションを分けたい
管理画面を作るときに例えば管理者とユーザーでログインするページが別々になっており、それぞれにロールとパーミッションの制御が必要になるということがよくあると思います。
laravel-permission
はデフォルトでconfig('auth.defaults.guard')
に指定されているGuardを参照しますがマルチガードに対応しております。
roles
テーブルもpermissions
テーブルもguard
カラムを持っているので、それぞれのロール、パーミッションを作成するときにどのガードか指定すればよいです。
以下はlaravel-permission
のREADMEに書いてあるマルチガードの対応例です。
// Create a superadmin role for the admin users
$role = Role::create(['guard_name' => 'admin', 'name' => 'superadmin']);
// Define a `publish articles` permission for the admin users belonging to the admin guard
$permission = Permission::create(['guard_name' => 'admin', 'name' => 'publish articles']);
// Define a *different* `publish articles` permission for the regular users belonging to the web guard
$permission = Permission::create(['guard_name' => 'web', 'name' => 'publish articles']);