Laravel · Bab 16
Lab 12: Otorisasi, Rate Limit & CORS
Membedakan siapa yang boleh melakukan apa dengan role, Gate, dan Policy (403), membatasi percobaan login dengan throttle (429), serta mengatur CORS agar hanya frontend yang dikenal boleh memanggil API dari browser.
Lab 11 menjawab pertanyaan “siapa kamu?” (autentikasi, gagal → 401). Lab ini menjawab “bolehkah kamu melakukan ini?” (otorisasi, gagal → 403). Seperti di bab 15, jawaban galat berbentuk JSON (401, 403, 429) mengandaikan klien mengirim Accept: application/json; Klien API di lab mengirimnya otomatis, dan perilaku tanpa header dijelaskan di bab 15. Kasir boleh menambah dan mengubah stok, tetapi hanya admin yang boleh menghapus produk. Setelah itu kita melindungi pintu login dari tebakan password beruntun, dan menyiapkan API agar bisa dipanggil frontend Vue dari browser. Contoh kode memakai studi kasus perpustakaan (books, anggota dan pustakawan); terapkan polanya ke Smart Retail.
Alur Langkah
- Tambahkan kolom
rolepadausersdan buat akun admin lewat seeder. - Buat
ProductPolicydan panggilGate::authorize()didestroy(). - Batasi percobaan login dengan
throttle:5,1. - Salin
config/cors.phpdan izinkan hanya asal frontend.
Role pada Pengguna
Cara paling sederhana membedakan hak akses adalah satu kolom role:
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
public function up(): void
{
Schema::table('users', function (Blueprint $table) {
$table->string('role')->default('anggota');
});
}
public function down(): void
{
if (Schema::hasColumn('users', 'role')) {
Schema::table('users', function (Blueprint $table) {
$table->dropColumn('role');
});
}
}
};
Bawaan default() membuat pengguna lama otomatis mendapat role paling rendah. Sama seperti api_token, kolom role tidak boleh masuk $fillable: kalau ada, siapa pun bisa mendaftar dengan {"role": "admin"} dan menaikkan haknya sendiri (privilege escalation). Lab ini tidak punya endpoint register, jadi risiko itu tidak diuji oleh cek lab; tetapi di aplikasi produksi role tidak boleh mass-assignable. Seeder mengisinya lewat properti, dan firstOrNew() membuatnya aman dijalankan ulang:
<?php
namespace Database\Seeders;
use App\Models\User;
use Illuminate\Database\Seeder;
use Illuminate\Support\Facades\Hash;
class LibrarianSeeder extends Seeder
{
public function run(): void
{
$pustakawan = User::firstOrNew(['email' => '[email protected]']);
$pustakawan->name = 'Kepala Perpustakaan';
$pustakawan->password = Hash::make('password123');
$pustakawan->role = 'pustakawan';
$pustakawan->save();
}
}
Gate dan Policy
Gate adalah aturan izin bernama yang tidak terikat model, biasanya didaftarkan di AppServiceProvider:
<?php
namespace App\Providers;
use App\Models\User;
use Illuminate\Support\Facades\Gate;
use Illuminate\Support\ServiceProvider;
class AppServiceProvider extends ServiceProvider
{
public function boot(): void
{
Gate::define('lihat-laporan', fn (User $user) => $user->role === 'pustakawan');
}
}
Policy mengelompokkan aturan untuk satu model. php artisan make:policy BookPolicy --model=Book membuat app/Policies/BookPolicy.php; Laravel menemukannya otomatis karena namanya mengikuti model (Book → BookPolicy). Cukup tulis method untuk aksi yang ingin dibatasi:
<?php
namespace App\Policies;
use App\Models\Book;
use App\Models\User;
class BookPolicy
{
public function delete(User $user, Book $book): bool
{
return $user->role === 'pustakawan';
}
}
Di controller, Gate::authorize() menjalankan aturan itu dan menghentikan request bila tidak diizinkan:
<?php
namespace App\Http\Controllers\Api\V1;
use App\Http\Controllers\Controller;
use App\Models\Book;
use Illuminate\Support\Facades\Gate;
class BookController extends Controller
{
public function destroy(Book $book)
{
Gate::authorize('delete', $book);
$book->delete();
return response()->noContent();
}
}
Base Controller Laravel 11 kosong, jadi $this->authorize() dari versi lama tidak tersedia; pakai Gate::authorize(). Untuk Gate bernama, panggil Gate::authorize('lihat-laporan'). Bila ditolak dan klien mengirim Accept: application/json, Laravel menjawab 403:
{ "message": "This action is unauthorized." }
Ingat bedanya (keduanya dalam bentuk JSON bila klien mengirim Accept: application/json): tanpa token → 401 (belum dikenal); dengan token tetapi role tidak cukup → 403 (dikenal, tetapi tidak boleh). Untuk akun admin yang boleh segalanya, cukup satu Gate::before() di AppServiceProvider yang mengembalikan true bagi role itu, tanpa menulis ulang setiap Policy. Klien Vue nanti memakai angka ini untuk memutuskan apakah membuka halaman login atau menampilkan pesan “akses ditolak”.
Rate Limiting dengan throttle
Tanpa batas, penyerang bisa mencoba ribuan password per menit ke POST /api/login. Middleware throttle:<jumlah>,<menit> membatasi jumlah request per klien. Untuk request yang belum login, “klien” berarti alamat IP: orang-orang di jaringan bersama (satu Wi-Fi, NAT) berbagi satu kuota yang sama.
<?php
use App\Http\Controllers\Api\AuthController;
use Illuminate\Support\Facades\Route;
Route::post('/login', [AuthController::class, 'login'])->middleware('throttle:5,1');
Request keenam dalam satu menit dijawab 429 (dengan Accept: application/json, berbentuk {"message": "Too Many Attempts."}). Setiap respons membawa header X-RateLimit-Limit dan X-RateLimit-Remaining, dan respons 429 menambah Retry-After (detik sampai boleh mencoba lagi). Hitungannya disimpan di cache Laravel; proyek ini memakai CACHE_STORE=database, jadi hitungan ada di tabel cache. Laravel 11 tidak memasang batas apa pun di rute API secara bawaan; kamu yang memutuskan endpoint mana yang perlu dibatasi. Untuk aturan yang lebih rumit, daftarkan limiter bernama dengan RateLimiter::for() di AppServiceProvider::boot() (Laravel 11), misalnya kuota per kombinasi email dan IP agar satu IP tidak menghabiskan jatah akun lain:
<?php
namespace App\Providers;
use Illuminate\Cache\RateLimiting\Limit;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\RateLimiter;
use Illuminate\Support\ServiceProvider;
class AppServiceProvider extends ServiceProvider
{
public function boot(): void
{
RateLimiter::for('login', fn (Request $request) => Limit::perMinute(5)->by($request->input('email').'|'.$request->ip()));
}
}
Pasang dengan ->middleware('throttle:login') pada rute login. Lab ini tetap memakai throttle:5,1 yang lebih sederhana.
CORS: Izin Lintas Asal untuk Browser
Frontend Vue nanti berjalan di http://localhost:5173, sedangkan API di http://127.0.0.1:8000. Bagi browser itu dua asal (origin) berbeda, dan browser menolak memberikan respons ke JavaScript kecuali server menyatakan asal itu diizinkan lewat header Access-Control-Allow-Origin. Inilah CORS (Cross-Origin Resource Sharing). Laravel 11 sudah memasang middleware CORS; salin konfigurasinya agar bisa diubah:
php artisan config:publish cors
<?php
return [
'paths' => ['api/*', 'sanctum/csrf-cookie'],
'allowed_methods' => ['*'],
'allowed_origins' => ['http://localhost:5173'],
'allowed_origins_patterns' => [],
'allowed_headers' => ['*'],
'exposed_headers' => [],
'max_age' => 0,
'supports_credentials' => false,
];
Bawaannya 'allowed_origins' => ['*'] (semua asal). Dua hal perlu dipahami. Pertama, browser menolak Access-Control-Allow-Origin: * bila request memakai kredensial (cookie atau sejenisnya), jadi 'supports_credentials' => true mewajibkan daftar origin eksplisit; jangan pernah memadukan * dengan kredensial. Kedua, header Authorization bukan header “sederhana”, sehingga browser lebih dulu mengirim request preflight OPTIONS untuk bertanya apakah request sebenarnya diizinkan. Middleware CORS Laravel menjawab preflight itu otomatis berdasarkan allowed_methods dan allowed_headers; kalau keduanya terlalu sempit, request asli tidak pernah dikirim browser. Untuk produksi, tulis asal frontend yang sebenarnya: skema, host, dan port, tanpa garis miring di akhir.
Klien API di lab hanya menampilkan status dan isi respons, bukan header. Tombol Cek yang memeriksa header Access-Control-Allow-Origin untukmu. Di laptop, header respons terlihat di tab Network pada DevTools browser.
Hasil yang Diharapkan
Jawaban 403 dan 429 di bawah mengandaikan header Accept: application/json (dikirim otomatis oleh Klien API di lab).
Kolom role ada dengan bawaan kasir, dan akun [email protected] ber-role admin dengan token admin-token-palsu-lab-12. DELETE /api/v1/products/2 dengan token kasir menjawab 403 dan produk tetap ada; dengan token admin menjawab 204. Percobaan login keenam dalam semenit menjawab 429 tanpa memblokir endpoint lain. Respons API membawa Access-Control-Allow-Origin: http://localhost:5173.