認可とアクセス制御
前回終了時のソースコード
Laravelを使ったアプリケーション開発のソースコードは以下のようにしてダウンロードすることができます。
$ git clone https://kiku3.tsbio.info/git/study-laravel.git study_laravel
この章開始時点のソースコードは chapt7 ブランチにあります。
$ git switch chapt7
ソースコードを自分で書いていく場合は、自分用のブランチをつくるとよいです。
$ git switch -c my7 chapt7
Laravelのコードはsrcディレクトリからの相対パスになっています。
認可 (Authorization) と
前章で、ユーザーの認証 (authentication) ができるようになりました。 次はどのユーザーは何ができるのかを認可 (authorization) するしくみをつくります。 Laravelでは役割 (role) と権限 (permission) でユーザーのアクセスを制御します。
権限はアプリケーションの機能(主にデータベースのテーブルに対する操作)に対して設定します。
oligoテーブルに対して、selectできる権限、insertできる権限、というよう感じです。
例えばselectであれば、Oligo::all() という形でプログラムの中に現れるので、この部分を実行できるかどうかというように設定します。
oligoテーブルに対してselectできる権限をoligo.readと名付けたとします。
この名称自体がなにかの効果を持つものではないことに注意してください。
権限名をプログラムに関連付けるのはプログラマーの責任です。
Oligo::all()があるのはOligoControllerのindexメソッドです。
oligo.readの権限をもつユーザーにのみ Oligo::all() を実行できるようにするために、if($user->can('oligo.read')) Oligo::all(); というようにコードを書きます。
認可は、ルーティングでコントローラーのメソッドへのアクセスを制御する方法、メソッド内でコードの実行を制御する方法、ビューで表示を制御する方法、などで実現できます。
ユーザーごとに権限を設定していくのは大変なので、いくつかの権限をセットにして役割 (role) とします。 この役割をユーザーにわりあてることで、ユーザーと権限が結び付けられ、権限の有無を判定できるようにします。
spatie/laravel-permissionというパッケージは permissions、rolesなどのテーブルをつくり、usersと結合できるようにします。
spatie/laravel-permissionをインストールする
spatie/laravel-permissionをインストールします。 続いてテーブルを作成します (migrate)。
# composer require spatie/laravel-permission # artisan vendor:publish --provider="Spatie\Permission\PermissionServiceProvider" # ./artizan migrate
ユーザーモデルの修正
Spatieが提供する権限の確認のためのメソッドをModel/Userに追加します。
app/Models/User.php
<?php
namespace App\Models;
// use Illuminate\Contracts\Auth\MustVerifyEmail;
use Database\Factories\UserFactory;
use Illuminate\Database\Eloquent\Attributes\Fillable;
use Illuminate\Database\Eloquent\Attributes\Hidden;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Foundation\Auth\User as Authenticatable;
use Illuminate\Notifications\Notifiable;
use Spatie\Permission\Traits\HasRoles; // 認可に関するメソッドが使えるようになる
#[Fillable(['name', 'email', 'password'])]
#[Hidden(['password', 'remember_token'])]
class User extends Authenticatable
{
/** @use HasFactory<UserFactory> */
use HasFactory, Notifiable, HasRoles; // HasRolesを追加
/**
* Get the attributes that should be cast.
*
* @return array<string, string>
*/
protected function casts(): array
{
return [
'email_verified_at' => 'datetime',
'password' => 'hashed',
];
}
}
テストデータを登録
tinkerで試します。
# ./artisan tinker
> use Spatie\Permission\Models\Permission
> $ow = Permission::create(['name' => 'oligo.write']) # oligo.writeという権限を追加
= Spatie\Permission\Models\Permission {#294
guard_name: "web",
name: "oligo.write",
updated_at: "2026-08-28 01:48:37",
created_at: "2026-08-28 01:48:37",
id: 25,
}
> $or = Permission::create(['name' => 'oligo.read']) # oligo.readという権限を追加
= Spatie\Permission\Models\Permission {#7546
guard_name: "web",
name: "oligo.read",
updated_at: "2026-08-28 01:48:48",
created_at: "2026-08-28 01:48:48",
id: 26,
}
> use Spatie\Permission\Models\Role
> $role = Role::create(['name' => 'oligo.user']) # oligo.userという役割を追加
= Spatie\Permission\Models\Role {#8044
guard_name: "web",
name: "oligo.user",
updated_at: "2026-08-28 01:49:36",
created_at: "2026-08-28 01:49:36",
id: 2,
}
> $role->syncPermissions($or) # oligo.userにoligo.readを付与
= Spatie\Permission\Models\Role {#8044
guard_name: "web",
name: "oligo.user",
updated_at: "2026-08-28 01:49:36",
created_at: "2026-08-28 01:49:36",
id: 2,
}
> use App\Models\User
> $user = User::first()
= App\Models\User {#8891
id: 2,
name: "test user",
email: "test@example.com",
email_verified_at: null,
#password: "\$2yxxx",
#remember_token: null,
created_at: "2026-08-26 02:14:30",
updated_at: "2026-08-26 02:14:30",
}
> $user->syncRoles($role) # test userにoligo.userという役割を付与
> $user->can('oligo.read') # test userはoligo.read権限を持つか? -> true
= true
> $user->can('oligo.write') # test userはoligo.write権限を持つか? -> false
= false
アクセス制御
ルーティング段階でのアクセス制御
ミドルウェアに can:oligo.read を追加する。 oligo.writeにするとアクセスできなくなる。
routes/oligo.php
<?php
use Illuminate\Support\Facades\Route;
use App\Http\Controllers\OligoController;
// オリゴDNAのデータベース
Route::middleware(['auth', 'verified', 'can:oligo.read'])
/* canはAuthorizeミドルウェアの別名として登録されている
* oligo.readをoligo.writeに変えると /oligo にアクセスできなくなる
*/
->prefix('/oligo')
->as('web.oligo.')
->group(function (){
Route::get('/', [OligoController::class, 'index'])->name('index');
Route::get('/search', [OligoController::class, 'search'])->name('search');
Route::get('/create', [OligoController::class, 'create'])->name('create');
Route::post('/', [OligoController::class, 'store'])->name('store');
// 編集フォームとデータ更新
Route::get('/edit', [OligoController::class, 'edit'])->name('edit');
Route::put('/{item}', [OligoController::class, 'update'])->name('update');
});
データ取得段階でのアクセス制御
RequestからUserを取得して、canを使う。 または Gateのauthorizeメソッドを使用する。
app/Http/Controllers/OligoController.php
<?php
namespace App\Http\Controllers;
use App\Models\Oligo;
use App\Services\OligoService;
use App\Http\Requests\OligoRequest;
use App\Http\Requests\OligoSearchRequest;
use App\Http\Requests\OligosRequest;
class OligoController
{
public function __construct(
private OligoService $service
) {
}
public function index(){
/*
* oligo.read権限が付与されていればデータを取得できる。
*/
if(request()->user()->can('oligo.read')){
$items = Oligo::all();
}else{
$items = [];
}
return view('oligo.index', compact('items'))
->fragmentIf(request()->hasHeader('HX-Request'), "feature-oligo-index")
;
}
public function create(){
return view('oligo.create')
->fragmentIf(request()->hasHeader('HX-Request'), "feature-oligo-create")
;
}
public function store(OligosRequest $request){
/*
* oligo.write権限がなければHTTP レスポンス 403を出して終了する
*/
\Illuminate\Support\Facades\Gate::authorize('oligo.write');
try{
$oligos = $request->validOligos;
$data = $this->service->create($oligos);
return response()->json(array_merge($data, [
'status' => 'success',
'input' => $oligos,
]), 200);
}catch(\Throwable $e){
return response()->json(['status' => 'error', 'message' => $e->getMessage()], 400);
}
}
// 検索
public function search(OligoSearchRequest $request)
{
try{
$query = $request->validated();
$query['use_regex'] = $query['use_regex'] ?? false;
return response()->json([
'status' => 'success',
'data' => $this->service->search($query),
], 200);
}catch(\Throwable $e){
return response()->json(['status' => 'error', 'message' => $e->getMessage()], 400);
}
}
public function edit(){
return view('oligo.edit')
->fragmentIf(request()->hasHeader('HX-Request'), "feature-oligo-edit")
;
}
public function update(OligoRequest $request, Oligo $item){
try{
$item->update($request->validated());
return response()->json([
'status' => 'success',
'item' => $item,
], 200);
}catch(\Throwable $e){
return response()->json(['status' => 'error', 'message' => $e->getMessage()], 400);
}
}
}
ビューでのアクセス制御
resources/views/oligo/module.blade.php
@extends('layouts.app')
@section('module')
@php($module = 'oligo') {{-- モジュール名を変数に設定 --}}
<x-module id='{{ $module }}'>
<!-- モジュールのメニュー -->
<nav>
<ul class='feature-tabs' role='tablist'>
@foreach([
{{-- フィーチャーごとの設定 --}}
'index' => ['label' => '一覧', 'permission' => 'read'],
'create' => ['label' => '追加', 'permission' => 'write'],
'edit' => ['label' => '編集', 'permission' => 'write'],
] as $page => $conf)
{{-- 権限の有無で項目を表示・非表示にする --}}
@can("$module.${conf['permission']}")
<li class='feature-tab'
:class="{
'bg-white': activeFeature === '{{ $module }}-{{ $page }}',
'bg-gray-400 hover:bg-gray-200': activeFeature !== '{{ $module }}-{{ $page }}'
}"
>
<a href='{{ route("web.{$module}.{$page}") }}' id='{{ $module }}-{{ $page }}-tab' role='tab'
@click.prevent="selectTab(event)"
>
{{ $conf['label'] }}
</a>
</li>
@endcan
@endforeach
<li class='feature-tab bg-gray-400' x-text="activeFeature">
</li>
</ul>
</nav>
<div class='features'
{{-- oligoモジュール全体で使用できるようにここにx-dataを定義する --}}
x-data="{
items: [], // テーブルから抽出されたデータ。今後更新や削除の機能でも使用できるように、ここ(#features)に定義する。
query: {}, // 検索の条件。x-modelとして設定することで入力フォームの値がそのままJavascriptで使用できる
// 検索 fetchは非同期(async)に実行されるので、それにそろえる
async search(){
const parameter = new URLSearchParams(this.query); // 同じx-data内なので this.queryとなる。これをGETパラメーター(?key1=value1&key2=value2)に変換する
console.log('parameter:', parameter.toString()); // 動作確認
const options = {
method: 'GET',
headers: {
'Accept': 'application/json', // 返り値の設定。Laravelが判断するのに使う。
'Content-Type': 'application/json',
},
};
return fetch('{{ route("web.{$module}.search") }}?' + parameter.toString(), options)
.then(response => response.json())
.then(response => {
console.log('search done', response); // 動作確認
return this.append(response.data.items); // itemsを置き換えるappendを呼ぶ
})
.catch(error => {debug(error)})
;
},
// 検索結果をitemsに反映させる。今後、新しい検索結果で置き換えるのではなく、追加するように修正していくので、appendとしてある。
append(items){
console.log('append:', items);
this.items = items;
},
// チェックがいれられたものだけを取得する。
get checked(){
return this.items.filter(item => item.checked);
},
get hasChecked(){
return this.checked.length > 0;
}
}"
>
@yield('feature')
</div>
</x-module>
@endsection
権限に関するウェブインターフェース
これはいずれつくるかもしれないが、後回しにする。