Aplikasi bisnis jarang punya satu jenis pengguna. Kasir hanya boleh mencatat transaksi, manajer boleh melihat laporan, dan admin boleh menghapus data. Kalau pembedaan ini ditulis dengan if ($user->role == 'admin') di mana-mana, setiap perubahan aturan berarti mencari dan mengedit puluhan file, dan hampir pasti ada yang terlewat.
Tutorial ini menyusun sistem akses yang rapi di Laravel 11: autentikasi dengan Laravel Breeze, lalu role dan permission dengan paket spatie/laravel-permission (versi 6). Kita bahas desain aksesnya dulu, baru kode, lalu error yang biasanya muncul di produksi.
1. Autentikasi vs Otorisasi
Dua istilah ini sering tertukar:
- Autentikasi menjawab "siapa Anda?". Ini urusan login, logout, reset password, dan verifikasi email.
- Otorisasi menjawab "Anda boleh melakukan apa?". Ini urusan role, permission, dan policy.
Laravel sudah menyediakan fondasi autentikasi. Spatie mengurus bagian otorisasi tanpa menggantikan sistem Gate bawaan Laravel, sehingga $user->can() dan @can tetap bekerja seperti biasa.
2. Memasang Autentikasi dengan Breeze
Untuk proyek baru, Breeze adalah titik awal paling ringan. Ia membuat halaman login, register, reset password, dan profil:
composer require laravel/breeze --dev
php artisan breeze:install blade
php artisan migrate
npm install && npm run build
Setelah itu route /login dan /register sudah aktif dan middleware auth siap dipakai. Untuk aplikasi internal seperti POS atau HRIS, pertimbangkan menonaktifkan register publik dan membuat akun hanya dari panel admin.
3. Rancang Akses Sebelum Menulis Kode
Langkah yang paling sering dilewati adalah membuat matriks akses. Tulis daftar aksi, lalu tentukan role mana yang boleh. Contoh untuk aplikasi kasir sederhana:
| Permission | Kasir | Manajer | Admin |
|---|---|---|---|
| transaksi.buat | Ya | Ya | Ya |
| transaksi.batal | Tidak | Ya | Ya |
| laporan.lihat | Tidak | Ya | Ya |
| produk.kelola | Tidak | Ya | Ya |
| pengguna.kelola | Tidak | Tidak | Ya |
Pola nama objek.aksi membuat daftar permission mudah dibaca dan diurutkan. Yang penting konsisten: jangan campur "edit articles" dengan "artikel.hapus".
4. Instalasi spatie/laravel-permission
composer require spatie/laravel-permission
php artisan vendor:publish --provider="Spatie\Permission\PermissionServiceProvider"
php artisan optimize:clear
php artisan migrate
Migration membuat lima tabel: roles, permissions, model_has_roles, model_has_permissions, dan role_has_permissions. Lalu tambahkan trait ke model User:
use Illuminate\Foundation\Auth\User as Authenticatable;
use Spatie\Permission\Traits\HasRoles;
class User extends Authenticatable
{
use HasRoles;
}
Trait ini menambahkan method seperti assignRole(), hasRole(), givePermissionTo(), dan syncRoles().
5. Seeder: Satu Sumber Kebenaran
Jangan membuat role lewat tinker di server produksi. Tulis matriks akses tadi sebagai seeder yang aman dijalankan berulang:
use Illuminate\Database\Seeder;
use Spatie\Permission\Models\Permission;
use Spatie\Permission\Models\Role;
use Spatie\Permission\PermissionRegistrar;
class RolePermissionSeeder extends Seeder
{
public function run(): void
{
app(PermissionRegistrar::class)->forgetCachedPermissions();
$matrix = [
'kasir' => ['transaksi.buat'],
'manajer' => ['transaksi.buat', 'transaksi.batal', 'laporan.lihat', 'produk.kelola'],
];
$all = collect($matrix)->flatten()->push('pengguna.kelola')->unique();
foreach ($all as $name) {
Permission::firstOrCreate(['name' => $name, 'guard_name' => 'web']);
}
foreach ($matrix as $role => $perms) {
Role::firstOrCreate(['name' => $role, 'guard_name' => 'web'])
->syncPermissions($perms);
}
Role::firstOrCreate(['name' => 'admin', 'guard_name' => 'web']);
}
}
syncPermissions() mengganti seluruh permission role sesuai array, jadi jika Anda mencabut izin dari matriks, menjalankan ulang seeder ikut mencabutnya di database. Role admin sengaja dibiarkan tanpa permission karena akan ditangani oleh Gate::before.
6. Super-Admin dengan Gate::before
Daripada memberi admin setiap permission satu per satu (dan lupa saat permission baru ditambah), daftarkan pengecualian di AppServiceProvider:
use Illuminate\Support\Facades\Gate;
public function boot(): void
{
Gate::before(function ($user, $ability) {
return $user->hasRole('admin') ? true : null;
});
}
Kembalikan null, bukan false, untuk pengguna lain. Nilai false akan menolak semua pengecekan untuk non-admin.
7. Melindungi Route dengan Middleware
Di Laravel 11, daftarkan alias middleware Spatie di bootstrap/app.php:
->withMiddleware(function (Middleware $middleware) {
$middleware->alias([
'role' => \Spatie\Permission\Middleware\RoleMiddleware::class,
'permission' => \Spatie\Permission\Middleware\PermissionMiddleware::class,
'role_or_permission' => \Spatie\Permission\Middleware\RoleOrPermissionMiddleware::class,
]);
})
Lalu gunakan di route:
Route::middleware('auth')->group(function () {
Route::post('/transaksi', [TransaksiController::class, 'store'])
->middleware('permission:transaksi.buat');
Route::get('/laporan', [LaporanController::class, 'index'])
->middleware('permission:laporan.lihat');
Route::resource('pengguna', UserController::class)
->middleware('permission:pengguna.kelola');
});
Perhatikan bahwa Gate::before tidak berlaku untuk middleware role:, karena middleware itu mengecek nama role secara langsung. Middleware permission: memakai Gate, sehingga admin tetap lolos. Ini satu alasan lagi untuk mengecek permission, bukan role.
8. Mengecek Akses di Controller dan Blade
// di controller
public function destroy(Transaksi $transaksi)
{
$this->authorize('transaksi.batal'); // butuh trait AuthorizesRequests
// atau: abort_unless(auth()->user()->can('transaksi.batal'), 403);
$transaksi->update(['status' => 'batal']);
return back();
}
Catatan: controller dasar Laravel 11 tidak lagi menyertakan trait AuthorizesRequests. Tambahkan trait itu ke app/Http/Controllers/Controller.php atau pakai Gate::authorize('transaksi.batal').
@can('laporan.lihat')
<a href="{{ route('laporan.index') }}">Laporan</a>
@endcan
Menyembunyikan tombol di Blade hanya soal tampilan. Pengamanan sebenarnya tetap di route atau controller, karena pengguna bisa memanggil URL langsung.
9. Permission Ditambah Policy
Permission menjawab "boleh membatalkan transaksi atau tidak". Ia tidak menjawab "boleh membatalkan transaksi milik cabang lain atau tidak". Untuk aturan yang bergantung pada data, gabungkan dengan Policy:
public function delete(User $user, Transaksi $trx): bool
{
return $user->can('transaksi.batal')
&& $trx->cabang_id === $user->cabang_id;
}
10. Error Umum dan Solusinya
- "There is no permission named X for guard web": nama permission salah ketik atau belum di-seed. Jalankan seeder dan cek ejaan.
- Perubahan izin tidak berlaku: cache permission. Jalankan
php artisan permission:cache-resetsetelah mengubah data. - Middleware tidak dikenal: alias belum didaftarkan di
bootstrap/app.php, atau namespace yang ditulis versi lama (Middlewaresdi v5,Middlewaredi v6). - Akses API selalu 403: permission dibuat untuk guard
websementara request memakai guard lain. Samakanguard_nameatau buat permission untuk guard tersebut.
11. Membuat Halaman Kelola Role untuk Admin
Setelah aplikasi berjalan, pemilik bisnis biasanya ingin mengatur sendiri siapa mendapat role apa tanpa memanggil developer. Halaman pengelolaan pengguna cukup memanggil syncRoles():
public function updateRole(Request $request, User $user)
{
Gate::authorize('pengguna.kelola');
$data = $request->validate([
'role' => ['required', 'exists:roles,name'],
]);
if ($user->is($request->user())) {
return back()->with('error', 'Anda tidak bisa mengubah role akun sendiri.');
}
$user->syncRoles([$data['role']]);
return back()->with('success', 'Role berhasil diperbarui.');
}
Ada dua pengaman di kode ini. Validasi exists:roles,name mencegah role palsu dikirim lewat form, dan pengecekan akun sendiri mencegah admin tanpa sengaja menurunkan dirinya lalu terkunci dari panel. Untuk aplikasi yang lebih sensitif, catat juga setiap perubahan role ke tabel log berisi siapa yang mengubah, untuk siapa, dan kapan.
Sebaiknya permission tetap dikelola lewat seeder oleh developer, sementara admin hanya memilih role. Jika admin juga bisa membuat permission bebas dari panel, nama permission di database bisa tidak cocok dengan yang dicek di kode, dan fitur menjadi tidak bisa diakses tanpa ada pesan error yang jelas.
Mengujinya
Test berikut memastikan matriks akses benar-benar diterapkan. Jalankan seeder di dalam test agar role dan permission tersedia:
it('kasir tidak bisa membuka laporan', function () {
$this->seed(RolePermissionSeeder::class);
$kasir = User::factory()->create()->assignRole('kasir');
$this->actingAs($kasir)->get('/laporan')->assertForbidden();
});
it('manajer bisa membuka laporan', function () {
$this->seed(RolePermissionSeeder::class);
$manajer = User::factory()->create()->assignRole('manajer');
$this->actingAs($manajer)->get('/laporan')->assertOk();
});
Checklist Implementasi
- Matriks akses tertulis dan disetujui pemilik bisnis.
- Semua role dan permission dibuat lewat seeder, bukan manual.
- Kode mengecek permission, bukan nama role.
- Setiap route sensitif punya middleware, bukan hanya tombol yang disembunyikan.
- Ada test yang memastikan role kasir mendapat 403 di halaman laporan.
Untuk lapisan keamanan lain, lihat checklist keamanan dasar Laravel dan 10 best practice keamanan aplikasi web. Jika aplikasi Anda melayani banyak klien sekaligus, desain role per tenant dibahas di membangun multi-tenant SaaS dengan Laravel.