認可とアクセス制御

前回終了時のソースコード

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

権限に関するウェブインターフェース

これはいずれつくるかもしれないが、後回しにする。