Pengguna sudah login, tetapi apakah ia boleh menghapus pesanan milik orang lain? Banyak aplikasi Laravel yang berhenti pada pengecekan "sudah login" lalu lupa memeriksa "boleh melakukan apa". Akibatnya pengguna biasa bisa mengubah data pengguna lain hanya dengan mengganti angka ID di URL. Laravel menyediakan Gate dan Policy khusus untuk masalah ini, dan memakainya jauh lebih rapi daripada menaruh if di setiap controller.
Autentikasi dan Otorisasi Bukan Hal yang Sama
- Autentikasi menjawab "siapa Anda?" Contohnya form login dan middleware
auth. - Otorisasi menjawab "apa yang boleh Anda lakukan?" Contohnya hanya pemilik pesanan yang boleh mengubahnya, atau hanya admin yang boleh menghapus produk.
Kesalahan otorisasi masuk daftar risiko teratas aplikasi web karena sulit terlihat saat pengembangan: semuanya tampak normal sampai ada orang yang mencoba URL milik pengguna lain. Untuk peran dan permission berbasis paket, lihat juga autentikasi dan role permission dengan Spatie.
Gate: Aturan Sederhana Berbentuk Closure
Gate cocok untuk aturan yang tidak terikat pada satu model, misalnya "hanya admin yang boleh membuka halaman laporan". Daftarkan di AppServiceProvider:
use Illuminate\Support\Facades\Gate;
use App\Models\User;
public function boot(): void
{
Gate::define('lihat-laporan', function (User $user) {
return $user->role === 'admin';
});
}
Pemakaiannya:
if (Gate::allows('lihat-laporan')) {
// tampilkan laporan
}
Gate::authorize('lihat-laporan'); // lempar 403 otomatis bila ditolak
Policy: Aturan Per Model
Begitu aturan menyangkut sebuah model (lihat, ubah, hapus pesanan), Policy lebih tepat karena semua aturan satu model terkumpul di satu kelas. Buat dengan:
php artisan make:policy PesananPolicy --model=Pesanan
Laravel mendeteksi pasangan Pesanan dan PesananPolicy secara otomatis. Isi method sesuai aturan bisnis:
class PesananPolicy
{
public function viewAny(User $user): bool
{
return true;
}
public function view(User $user, Pesanan $pesanan): bool
{
return $user->id === $pesanan->user_id || $user->role === 'admin';
}
public function update(User $user, Pesanan $pesanan): bool
{
// Pesanan yang sudah dibayar tidak boleh diubah siapa pun.
return $user->id === $pesanan->user_id && $pesanan->status === 'baru';
}
public function delete(User $user, Pesanan $pesanan): bool
{
return $user->role === 'admin';
}
}
Perhatikan method update: otorisasi bukan hanya soal peran, tetapi juga status data. Aturan seperti ini sangat sering diperlukan dan paling rapi ditulis di policy.
Memakai Policy di Controller
public function update(Request $request, Pesanan $pesanan)
{
$this->authorize('update', $pesanan); // 403 jika tidak boleh
$pesanan->update($request->validated());
return redirect()->route('pesanan.show', $pesanan);
}
Untuk controller resource, satu baris di konstruktor cukup menjaga semua method standar sekaligus:
public function __construct()
{
$this->authorizeResource(Pesanan::class, 'pesanan');
}
Kini index, show, update, dan destroy otomatis diperiksa ke method policy yang sesuai.
Di Blade dan Route
@can('update', $pesanan)
<a href="{{ route('pesanan.edit', $pesanan) }}">Ubah</a>
@endcan
Route::put('/pesanan/{pesanan}', [PesananController::class, 'update'])
->middleware('can:update,pesanan');
Menyembunyikan tombol saja tidak cukup. Tombol hanya soal tampilan; yang melindungi data adalah pengecekan di sisi server (controller atau middleware). Selalu pasang keduanya.
Mengizinkan Admin Tanpa Menulis Ulang Setiap Method
Metode before di policy dijalankan lebih dulu. Cocok untuk memberi akses penuh pada super admin:
public function before(User $user, string $ability): ?bool
{
if ($user->role === 'super_admin') {
return true; // lolos tanpa cek lain
}
return null; // lanjut ke method policy biasa
}
Kembalikan null (bukan false) agar pengecekan berlanjut untuk pengguna lain.
Pesan Penolakan yang Jelas
Secara bawaan penolakan menghasilkan halaman 403 generik. Untuk aturan yang sering membingungkan pengguna, kembalikan alasan lewat Response::deny() agar mereka tahu apa yang harus dilakukan:
use Illuminate\Auth\Access\Response;
public function update(User $user, Pesanan $pesanan): Response
{
if ($pesanan->status !== 'baru') {
return Response::deny('Pesanan yang sudah dibayar tidak dapat diubah.');
}
return $user->id === $pesanan->user_id
? Response::allow()
: Response::denyAsNotFound(); // sembunyikan keberadaan data
}
denyAsNotFound() berguna untuk data sensitif: pengguna yang tidak berhak menerima 404, bukan 403, sehingga ia tidak bisa menebak ID mana yang ada di sistem.
Menyaring Daftar, Bukan Hanya Satu Data
Policy memeriksa satu data pada satu waktu. Untuk halaman daftar, filter langsung di query agar pengguna hanya menerima data miliknya, dan jangan mengandalkan pengecekan satu per satu setelah semua data diambil:
public function index(Request $request)
{
$query = Pesanan::query()->latest();
if ($request->user()->role !== 'admin') {
$query->where('user_id', $request->user()->id);
}
return view('pesanan.index', ['pesanan' => $query->paginate(20)]);
}
Pola ini lebih efisien dan menghindari kebocoran lewat jumlah total, hitungan halaman, atau pencarian. Gabungkan dengan optimasi query Eloquent agar daftar tetap cepat.
Aksi Tanpa Model: create dan viewAny
Untuk aksi yang belum punya instance model, seperti membuat data baru, teruskan nama kelasnya:
$this->authorize('create', Pesanan::class);
@can('create', App\Models\Pesanan::class)
<a href="{{ route('pesanan.create') }}">Buat Pesanan</a>
@endcan
Method create(User $user) di policy tidak menerima parameter model, sehingga cocok untuk aturan seperti "hanya pengguna yang paketnya masih aktif boleh membuat pesanan".
Menguji Otorisasi
public function test_pengguna_tidak_bisa_mengubah_pesanan_orang_lain(): void
{
$pemilik = User::factory()->create();
$penyusup = User::factory()->create();
$pesanan = Pesanan::factory()->for($pemilik)->create();
$this->actingAs($penyusup)
->put("/pesanan/{$pesanan->id}", ['catatan' => 'diubah'])
->assertForbidden();
}
Satu test seperti ini menangkap kebocoran data yang tidak akan terlihat saat Anda mencoba manual dengan akun sendiri. Pelajari dasar pengujiannya di testing otomatis di Laravel.
Alur Kerja yang Disarankan
- Daftar semua aksi yang bisa dilakukan pada sebuah model: lihat, buat, ubah, hapus, dan aksi khusus seperti menyetujui atau membatalkan.
- Tulis aturan tiap aksi dalam satu kalimat bahasa sehari-hari sebelum menulis kode, misalnya "hanya pemilik, selama status masih baru".
- Terjemahkan kalimat itu ke method policy, lalu tulis satu test untuk kasus diizinkan dan satu untuk kasus ditolak.
- Pasang
authorizedi controller dan@candi Blade, kemudian periksa daftar halaman agar data difilter sejak query.
Dengan langkah ini, aturan akses terdokumentasi lewat test dan mudah ditinjau ulang oleh anggota tim lain saat aplikasi bertambah besar.
Kesalahan Umum
- Hanya memakai middleware
authdan mengira sudah aman. - Mengambil data dengan
Pesanan::find($id)tanpa pengecekan pemilik. Alternatifnya:auth()->user()->pesanan()->findOrFail($id). - Menyembunyikan menu di Blade tetapi route-nya tidak dilindungi.
- Mengembalikan
falsedaribeforesehingga mematikan seluruh policy. - Tidak menulis test untuk kasus penolakan, hanya kasus berhasil.
Otorisasi yang baik adalah bagian dari checklist keamanan aplikasi Laravel. Mulailah dari model yang menyimpan data pribadi atau uang, lalu perluas ke seluruh aplikasi.