Autentikasi membuktikan identitas pengguna yang mengirimkan permintaan, sedangkan otorisasi (authorization) menentukan hal yang boleh dilakukan oleh identitas tersebut, misalnya menghapus kategori atau mengelola data aplikasi. Topik ini membahas dua fasilitas otorisasi pada Laravel, yaitu gate untuk aturan yang hanya bergantung pada pengguna dan policy untuk aturan yang bergantung pada model yang menjadi sasaran aksi. Pemeriksaan otorisasi pada controller dan direktif @can pada tampilan Blade diuji langsung pada aplikasi catatan. Setelah topik ini dibaca, pembatasan hak akses dapat ditempatkan secara konsisten pada controller dan tampilan.
Gate untuk aturan berbasis pengguna#
Gate adalah kemampuan bernama yang didefinisikan sekali lalu diperiksa di mana pun dibutuhkan. Definisinya ditempatkan pada metode boot() di berkas app/Providers/AppServiceProvider.php, yaitu metode yang dijalankan Laravel pada setiap siklus permintaan. Metode Gate::define() menerima nama kemampuan dan sebuah fungsi yang mengembalikan nilai true atau false, sedangkan fungsi tersebut menerima pengguna yang sedang diperiksa. Aplikasi catatan membatasi pengelolaan kategori pada pengguna yang sudah memverifikasi alamat emailnya, sehingga gate kelola-kategori didefinisikan seperti berikut.
// tidak-dijalankan
use App\Models\User;
use Illuminate\Support\Facades\Gate;
Gate::define('kelola-kategori', function (User $pengguna) {
return $pengguna->email_verified_at !== null;
});Fungsi tersebut membaca kolom email_verified_at pada tabel users, yaitu kolom yang berisi tanggal verifikasi yang bernilai null selama email belum diverifikasi. Perilakunya dapat diamati tanpa HTTP dengan mendefinisikan gate yang sama di dalam tinker, lalu memeriksa dua pengguna, yaitu Ali yang terverifikasi dan Budi yang belum. Pemanggilan $pengguna->can() mengembalikan nilai boolean, sedangkan Gate::authorize() melemparkan exception, yaitu objek yang menandai kondisi kesalahan untuk ditangani oleh handler, ketika pemeriksaan ditolak.
# mendefinisikan gate kelola-kategori lalu mengujinya pada dua pengguna
cd ~/aplikasi-catatan
php artisan tinker --execute='
use App\Models\User;
use Illuminate\Support\Facades\Gate;
Gate::define("kelola-kategori", function (User $pengguna) {
return $pengguna->email_verified_at !== null;
});
$ali = User::updateOrCreate(
["email" => "[email protected]"],
["name" => "Ali", "password" => "sandi-ali"]
);
$ali->forceFill(["email_verified_at" => now()])->save();
$budi = User::updateOrCreate(
["email" => "[email protected]"],
["name" => "Budi", "password" => "sandi-budi"]
);
$budi->forceFill(["email_verified_at" => null])->save();
echo "Ali: ";
var_dump($ali->can("kelola-kategori"));
echo "Budi: ";
var_dump($budi->can("kelola-kategori"));
try {
Gate::forUser($budi)->authorize("kelola-kategori");
} catch (\Illuminate\Auth\Access\AuthorizationException $e) {
echo get_class($e) . PHP_EOL;
}
'Ali: bool(true)
Budi: bool(false)
Illuminate\Auth\Access\AuthorizationExceptionBaris forceFill() pada persiapan data mengisi email_verified_at secara langsung karena atribut tersebut berada di luar daftar $fillable milik model User, sehingga updateOrCreate() tidak menyentuhnya. Keluaran tersebut memperlihatkan aturan yang berjalan sesuai definisi, yaitu Ali yang terverifikasi diterima dan Budi yang belum ditolak. Pemanggilan authorize() pada Budi berakhir dengan AuthorizationException, dan pada permintaan HTTP nyata handler Laravel mengubah exception tersebut menjadi respons 403 Forbidden. Dengan demikian, satu panggilan authorize() pada awal aksi controller sudah cukup untuk menghentikan permintaan yang tidak berhak.
Policy untuk aturan yang bergantung pada model#
Policy adalah kelas yang mengelompokkan aturan otorisasi atas satu model, dengan setiap metodenya mewakili satu aksi. Gate menilai pengguna saja, sedangkan metode policy menerima model yang menjadi sasaran aksi sehingga aturan dapat membaca isinya. Konvensi penamaannya tegas, yaitu kelas App\Policies\CategoryPolicy untuk model App\Models\Category, dan kelas yang mengikuti konvensi tersebut ditemukan secara otomatis tanpa registrasi. Aplikasi catatan melarang penghapusan kategori yang masih memuat catatan, sehingga policy yang mengaturnya berisi metode delete seperti berikut.
// tidak-dijalankan
<?php
namespace App\Policies;
use App\Models\Category;
use App\Models\User;
class CategoryPolicy
{
public function delete(User $pengguna, Category $kategori): bool
{
return $kategori->notes()->doesntExist();
}
}Metode delete menerima pengguna dan kategori yang diperiksa, lalu mengembalikan nilai $kategori->notes()->doesntExist(), yaitu query yang memeriksa apakah relasi hasMany menuju Note benar-benar kosong. Perilakunya kembali diuji pada dua kategori, yaitu Kerja yang memuat catatan dan Arsip yang kosong. Pemeriksaan dilakukan oleh Citra, pengguna contoh yang dibuat melalui firstOrCreate. Karena proyek uji tidak menyimpan berkas policy secara permanen, perintah berikut menyalin berkas CategoryPolicy.php ke app/Policies pada awal eksekusi dan menghapusnya kembali pada akhir perintah, sehingga blok yang sama dapat dijalankan ulang dari keadaan bersih.
# menyiapkan kategori contoh lalu menguji policy delete
cd ~/aplikasi-catatan
mkdir -p app/Policies
cat > app/Policies/CategoryPolicy.php <<'POLICY'
<?php
namespace App\Policies;
use App\Models\Category;
use App\Models\User;
class CategoryPolicy
{
public function delete(User $pengguna, Category $kategori): bool
{
return $kategori->notes()->doesntExist();
}
}
POLICY
php artisan tinker --execute='
use App\Models\Category;
use App\Models\Note;
use App\Models\User;
Note::whereIn("category_id", Category::where("nama", "Arsip")->pluck("id"))->delete();
$kerja = Category::updateOrCreate(["nama" => "Kerja"]);
$arsip = Category::updateOrCreate(["nama" => "Arsip"]);
Note::updateOrCreate(
["judul" => "Rapat pekanan"],
["isi" => "Menyiapkan poin diskusi dan menandai keputusan rapat.", "category_id" => $kerja->id]
);
$pengguna = User::firstOrCreate(
["email" => "[email protected]"],
["name" => "Citra", "password" => "sandi-citra"]
);
echo "Kategori Kerja: ";
var_dump($pengguna->can("delete", $kerja));
echo "Kategori Arsip: ";
var_dump($pengguna->can("delete", $arsip));
'
rm app/Policies/CategoryPolicy.phpKategori Kerja: bool(false)
Kategori Arsip: bool(true)Pemanggilan can('delete', $kategori) mengenali argumen kedua berupa model Category, mencari CategoryPolicy melalui konvensi nama, lalu memanggil metode delete dengan pengguna dan model tersebut sebagai argumen. Kategori Kerja ditolak karena relasinya menemukan satu catatan, sedangkan kategori Arsip diterima karena kosong. Apabila delete diperiksa tanpa model, pemeriksaan berhenti pada gate dan selalu ditolak karena tidak ada gate dengan nama tersebut.
Pemeriksaan otorisasi pada controller#
Pemeriksaan yang paling menentukan berada pada controller karena semua permintaan, termasuk yang dikirim tanpa melalui tampilan, melewati controller tersebut. Metode Gate::authorize() menerima nama kemampuan tanpa model untuk gate, atau nama kemampuan disertai model untuk policy, kemudian melemparkan AuthorizationException apabila ditolak. Handler exception bawaan mengubah exception tersebut menjadi respons 403 tanpa kode tambahan. Contoh penggunaannya pada aksi yang menyimpan kategori baru berbentuk berikut.
// tidak-dijalankan
public function store(Request $request): RedirectResponse
{
Gate::authorize('kelola-kategori');
$kategori = Category::create($request->validate([
'nama' => ['required', 'string', 'max:100'],
]));
return redirect()->route('dashboard');
}Bentuk Gate::authorize() mengembalikan nilai true apabila pemeriksaan diterima, tetapi nilai kembalian itu jarang digunakan karena eksekusi berlanjut ke baris berikutnya. Apabila kendali respons dibutuhkan, misalnya untuk menampilkan pesan tertentu, metode can() mengembalikan nilai boolean dan tindak lanjutnya ditentukan sendiri oleh controller. Kombinasi yang sama berlaku untuk policy, yaitu Gate::authorize('delete', $kategori) atau $request->user()->can('delete', $kategori). Posisi pemeriksaan pada aksi controller menjamin aturan yang sama dijalankan untuk permintaan dari browser maupun permintaan langsung tanpa tampilan.
Direktif @can pada tampilan Blade#
Direktif @can membawa pemeriksaan yang sama ke dalam tampilan Blade, sehingga elemen antarmuka hanya dirender bagi pengguna yang berhak. Direktif ini menerima nama kemampuan, disertai model apabila kemampuannya berasal dari policy, dan pasangan @endcan menutup bloknya. Bentuk lainnya juga tersedia, yaitu @elsecan untuk kondisi tambahan dan @cannot untuk pembalikan pemeriksaan. Cuplikan berikut menyembunyikan tombol hapus dari pengguna yang tidak lolos pemeriksaan policy delete.
<ul>
@foreach ($daftarKategori as $kategori)
<li>
{{ $kategori->nama }}
@can('delete', $kategori)
<form method="POST" action="{{ route('kategori.destroy', $kategori) }}">
@csrf
<button type="submit">Hapus</button>
</form>
@endcan
</li>
@endforeach
</ul>Perilaku direktif ini dapat diamati dengan merender tampilan sementara untuk dua pengguna. Tampilan ujinya memuat tiga pemeriksaan, yaitu gate kelola-kategori tanpa model, policy delete pada kategori Arsip yang kosong, dan policy delete pada kategori Kerja yang memuat catatan. Definisi gate dituliskan ulang di dalam tinker karena proses uji berlangsung secara mandiri, sedangkan pada aplikasi nyata definisi di AppServiceProvider sudah tersedia untuk setiap permintaan. Berkas policy dan tampilan uji sama-sama dibuat sementara oleh perintahnya dan dihapus setelah keluaran didapat.
# membuat policy dan tampilan sementara lalu merender direktif @can untuk dua pengguna
cd ~/aplikasi-catatan
mkdir -p app/Policies
cat > app/Policies/CategoryPolicy.php <<'POLICY'
<?php
namespace App\Policies;
use App\Models\Category;
use App\Models\User;
class CategoryPolicy
{
public function delete(User $pengguna, Category $kategori): bool
{
return $kategori->notes()->doesntExist();
}
}
POLICY
cat > resources/views/demo-can.blade.php <<'BLADE'
@can('kelola-kategori')<p>Kelola kategori</p>@endcan @can('delete', $arsip)<p>Hapus arsip</p>@endcan @can('delete', $kerja)<p>Hapus kerja</p>@endcan
BLADE
php artisan tinker --execute='
use App\Models\Category;
use App\Models\Note;
use App\Models\User;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Gate;
use Illuminate\Support\Facades\View;
Gate::define("kelola-kategori", function (User $pengguna) {
return $pengguna->email_verified_at !== null;
});
$ali = User::updateOrCreate(
["email" => "[email protected]"],
["name" => "Ali", "password" => "sandi-ali"]
);
$ali->forceFill(["email_verified_at" => now()])->save();
$budi = User::updateOrCreate(
["email" => "[email protected]"],
["name" => "Budi", "password" => "sandi-budi"]
);
$budi->forceFill(["email_verified_at" => null])->save();
$kerja = Category::updateOrCreate(["nama" => "Kerja"]);
$arsip = Category::updateOrCreate(["nama" => "Arsip"]);
Note::updateOrCreate(
["judul" => "Rapat pekanan"],
["isi" => "Menyiapkan poin diskusi dan menandai keputusan rapat.", "category_id" => $kerja->id]
);
Auth::setUser($ali);
echo "Ali: " . trim(View::make("demo-can", ["arsip" => $arsip, "kerja" => $kerja])->render()) . PHP_EOL;
Auth::setUser($budi);
echo "Budi: " . trim(View::make("demo-can", ["arsip" => $arsip, "kerja" => $kerja])->render()) . PHP_EOL;
'
rm resources/views/demo-can.blade.php
rm app/Policies/CategoryPolicy.phpAli: <p>Kelola kategori</p> <p>Hapus arsip</p>
Budi: <p>Hapus arsip</p>Keluaran tersebut menunjukkan tiga hal sekaligus. Elemen Kelola kategori hanya tampil untuk Ali karena gate menilai verifikasi emailnya, elemen Hapus arsip tampil untuk keduanya karena policy hanya menilai kosongnya kategori, dan elemen Hapus kerja tidak tampil untuk siapa pun karena kategori Kerja masih memuat catatan. Apabila aturan pengguna perlu digabungkan dengan aturan model, kedua pemeriksaan dituliskan dalam satu metode policy yang sama.
Kesalahan umum#
Kesalahan pertama adalah menamai policy di luar konvensi, misalnya membuat kelas KategoriPolicy untuk model Category. Laravel mencari kelas CategoryPolicy dan tidak menemukannya, sehingga setiap kemampuan pada kelas tersebut selalu ditolak tanpa pesan yang menjelaskan penyebabnya. Perbaikannya adalah mengikuti konvensi App\Policies\<NamaModel>Policy atau mendaftarkannya secara manual dengan Gate::policy(Category::class, KategoriPolicy::class) pada AppServiceProvider.
Kesalahan kedua adalah menganggap kelas policy bekerja begitu berkasnya ada. Policy hanya dieksekusi ketika ada pemeriksaan yang memanggilnya, baik can(), Gate::authorize(), maupun @can, sehingga kelas policy yang tidak pernah dipanggil oleh satu pun pemeriksaan tidak melindungi apa pun. Pastikan setiap aksi yang perlu dilindungi memanggil salah satu bentuk pemeriksaan tersebut.
Kesalahan ketiga adalah mendefinisikan gate pada tempat yang tidak dijalankan setiap permintaan, misalnya di dalam metode controller atau di tinker. Definisi semacam ini hanya berlaku pada konteks yang menjalankannya, sehingga kemampuan tidak dikenal pada permintaan lain dan pemeriksaannya selalu ditolak. Definisikan gate pada metode boot() di AppServiceProvider agar tersedia untuk seluruh aplikasi.
Ringkasan#
- Gate menetapkan aturan yang hanya bergantung pada pengguna melalui
Gate::define()padaAppServiceProvider, dan diperiksa dengancan(),Gate::allows(), atauGate::authorize(). - Policy mengelompokkan aturan atas satu model dalam kelas yang mengikuti konvensi
App\Policies\<NamaModel>Policy, dan metodenya menerima model yang menjadi sasaran aksi. Gate::authorize()melemparkanAuthorizationExceptionyang diubah handler menjadi respons 403, sehingga aksi controller berhenti sebelum logikanya dijalankan.- Direktif
@canmenyembunyikan bagian tampilan sesuai hak akses, sedangkan pemeriksaan pada controller tetap menjadi pelindung utama karena tampilan dapat dilewati.
Referensi#
- Authorization — Laravel — Dokumentasi resmi gate, policy, dan pemanggilannya pada controller serta Blade.
- Blade — Laravel
— Dokumentasi direktif
@can,@elsecan, dan@cannotpada templat.