Di bagian sebelumnya kita sudah menguasai autentikasi — memastikan pengguna sudah login. Sekarang kita naik satu level: authorization — memastikan pengguna yang sudah login memiliki izin untuk melakukan aksi tertentu. Dua pertanyaan yang berbeda: "Siapa kamu?" adalah autentikasi, "Apa yang boleh kamu lakukan?" adalah authorization. Laravel menyediakan dua sistem authorization yang elegan — Gates untuk pengecekan sederhana dan Policies untuk authorization berbasis Model yang terstruktur.

💡 Ini adalah bagian kesepuluh dari seri "Belajar Laravel dari Nol". Bagian ini membangun langsung di atas Bagian 9 (Authentication). Pastikan kamu sudah memahami middleware auth dan sistem autentikasi Laravel sebelum melanjutkan ke authorization.


Apa Itu Middleware?

Middleware adalah lapisan yang memfilter HTTP request sebelum sampai ke Controller. Bayangkan middleware seperti pos pemeriksaan — setiap request harus melewati satu atau beberapa pos sebelum bisa diproses. Di setiap pos, request bisa diteruskan, dimodifikasi, atau ditolak sama sekali. Laravel sudah menyertakan middleware bawaan yang sangat berguna, dan kamu bisa membuat middleware kustom sesuai kebutuhan.

"Middleware provide a convenient mechanism for inspecting and filtering HTTP requests entering your application."

— — Dokumentasi Resmi Laravel

Middleware Bawaan Laravel

Laravel sudah menyertakan berbagai middleware yang menangani kebutuhan umum. Beberapa di antaranya sudah aktif secara global untuk setiap request, sementara yang lain perlu ditambahkan secara eksplisit ke route tertentu.

PHP
<?php
// bootstrap/app.php — Konfigurasi middleware di Laravel 11+

use Illuminate\Foundation\Application;
use Illuminate\Foundation\Configuration\Exceptions;
use Illuminate\Foundation\Configuration\Middleware;

return Application::configure(basePath: dirname(__DIR__))
    ->withMiddleware(function (Middleware $middleware) {

        // ── MIDDLEWARE GLOBAL (dijalankan untuk setiap request) ──
        // Sudah aktif secara default:
        // - TrimStrings: trim whitespace dari semua input
        // - ConvertEmptyStringsToNull: string kosong jadi null
        // - HandleCors: menangani CORS headers
        // - ValidateCsrfToken: validasi CSRF token (web)

        // ── MIDDLEWARE ALIAS ──────────────────────────────────────
        // Alias yang sudah tersedia tanpa konfigurasi tambahan:
        // 'auth'          → Pastikan user sudah login
        // 'auth.basic'    → HTTP Basic Authentication
        // 'auth.session'  → Validasi session
        // 'cache.headers' → Set cache headers
        // 'can'           → Authorization via Gate
        // 'guest'         → Hanya untuk tamu (belum login)
        // 'password.confirm' → Konfirmasi password sebelum aksi sensitif
        // 'signed'        → Validasi signed URL
        // 'throttle'      → Rate limiting
        // 'verified'      → Email sudah diverifikasi

        // Daftarkan middleware kustom dengan alias:
        $middleware->alias([
            'role'       => \App\Http\Middleware\CheckRole::class,
            'ownership'  => \App\Http\Middleware\CheckOwnership::class,
            'active'     => \App\Http\Middleware\CheckUserIsActive::class,
        ]);

        // Tambahkan middleware ke grup tertentu:
        $middleware->appendToGroup('web', [
            \App\Http\Middleware\TrackLastActivity::class,
        ]);
    })
    ->create();

Membuat Middleware Kustom

Selain middleware bawaan, kamu bisa membuat middleware kustom untuk kebutuhan spesifik aplikasimu — misalnya mengecek role pengguna, memastikan akun masih aktif, mencatat aktivitas, atau menambahkan header tertentu ke setiap response.

#
Buat Middleware dengan Artisan

Artisan membuat file middleware di app/Http/Middleware/ dengan struktur dasar yang sudah siap. Method handle() menerima request dan closure $next yang harus dipanggil untuk meneruskan request ke middleware atau Controller berikutnya.

#
Tulis Logika Middleware

Di dalam method handle(), lakukan pengecekan atau modifikasi yang diperlukan. Jika kondisi tidak terpenuhi, kembalikan redirect atau response error. Jika kondisi terpenuhi, panggil $next($request) untuk meneruskan request ke tahap berikutnya.

#
Daftarkan dan Gunakan Middleware

Daftarkan middleware dengan alias di bootstrap/app.php, lalu gunakan alias tersebut di route definition atau di constructor Controller.

Bash
# Buat middleware baru
php artisan make:middleware CheckRole
php artisan make:middleware CheckUserIsActive
php artisan make:middleware LogUserActivity
php artisan make:middleware EnsureAccountNotSuspended

# File dibuat di: app/Http/Middleware/
PHP
<?php
// app/Http/Middleware/CheckRole.php

namespace App\Http\Middleware;

use Closure;
use Illuminate\Http\Request;
use Symfony\Component\HttpFoundation\Response;

class CheckRole
{
    /**
     * Handle an incoming request.
     *
     * @param  \Closure(\Illuminate\Http\Request): (\Symfony\Component\HttpFoundation\Response) $next
     */
    public function handle(Request $request, Closure $next, string ...$roles): Response
    {
        // Pastikan user sudah login
        if (!auth()->check()) {
            return redirect()->route('login');
        }

        $user = auth()->user();

        // Cek apakah user memiliki salah satu role yang diizinkan
        foreach ($roles as $role) {
            if ($user->hasRole($role)) {
                return $next($request); // Izinkan request
            }
        }

        // Jika tidak punya role yang dibutuhkan
        abort(403, 'Kamu tidak memiliki akses ke halaman ini.');
    }
}

// ── PENGGUNAAN DI ROUTE ────────────────────────────────
// routes/web.php
Route::middleware(['auth', 'role:admin'])->group(function () {
    Route::get('/admin', fn() => view('admin.dashboard'));
});

// Beberapa role yang diizinkan
Route::middleware(['auth', 'role:admin,editor'])->group(function () {
    Route::resource('posts', Admin\PostController::class);
});
PHP
<?php
// app/Http/Middleware/CheckUserIsActive.php

namespace App\Http\Middleware;

use Closure;
use Illuminate\Http\Request;
use Symfony\Component\HttpFoundation\Response;
use Illuminate\Support\Facades\Auth;

class CheckUserIsActive
{
    public function handle(Request $request, Closure $next): Response
    {
        if (auth()->check() && !auth()->user()->is_active) {
            // Logout user yang sudah di-suspend
            Auth::logout();
            $request->session()->invalidate();
            $request->session()->regenerateToken();

            return redirect()->route('login')
                ->with('error', 'Akun kamu telah dinonaktifkan. Hubungi administrator.');
        }

        return $next($request);
    }
}

// ── MIDDLEWARE YANG BERJALAN SETELAH REQUEST ───────────
// Middleware bisa juga memodifikasi RESPONSE (bukan hanya request)

class AddSecurityHeaders
{
    public function handle(Request $request, Closure $next): Response
    {
        // Teruskan request ke Controller dulu
        $response = $next($request);

        // Modifikasi response sebelum dikirim ke browser
        $response->headers->set('X-Frame-Options', 'SAMEORIGIN');
        $response->headers->set('X-Content-Type-Options', 'nosniff');
        $response->headers->set('X-XSS-Protection', '1; mode=block');
        $response->headers->set(
            'Content-Security-Policy',
            "default-src 'self'; script-src 'self' 'unsafe-inline'"
        );

        return $response;
    }
}

// ── MIDDLEWARE UNTUK LOGGING AKTIVITAS ────────────────
class LogUserActivity
{
    public function handle(Request $request, Closure $next): Response
    {
        $response = $next($request);

        // Log setelah request diproses
        if (auth()->check()) {
            activity()
                ->causedBy(auth()->user())
                ->withProperties([
                    'ip'     => $request->ip(),
                    'url'    => $request->fullUrl(),
                    'method' => $request->method(),
                    'status' => $response->getStatusCode(),
                ])
                ->log('accessed ' . $request->path());
        }

        return $response;
    }
}

Rate Limiting: Throttle Middleware

Rate limiting adalah teknik membatasi jumlah request yang bisa dilakukan dalam periode waktu tertentu — sangat penting untuk mencegah abuse, brute force attack, dan melindungi endpoint yang mahal secara komputasi. Laravel menyediakan sistem rate limiting yang sangat fleksibel melalui RateLimiter facade.

PHP
<?php
// app/Providers/AppServiceProvider.php — Definisi rate limiter kustom

use Illuminate\Cache\RateLimiting\Limit;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\RateLimiter;

public function boot(): void
{
    // ── RATE LIMITER KUSTOM ────────────────────────────
    // Rate limiter berdasarkan user ID (jika login) atau IP (jika tamu)
    RateLimiter::for('api', function (Request $request) {
        return Limit::perMinute(60)->by($request->user()?->id ?: $request->ip());
    });

    // Rate limiter yang berbeda untuk user premium
    RateLimiter::for('api-premium', function (Request $request) {
        if ($request->user()?->is_premium) {
            return Limit::perMinute(300)->by($request->user()->id);
        }

        return Limit::perMinute(60)->by(
            $request->user()?->id ?: $request->ip()
        );
    });

    // Rate limiter untuk form yang sensitif
    RateLimiter::for('contact-form', function (Request $request) {
        return [
            Limit::perMinute(3)->by($request->ip()),           // Maks 3/menit per IP
            Limit::perHour(10)->by($request->ip()),            // Maks 10/jam per IP
        ];
    });

    // Rate limiter dengan custom response
    RateLimiter::for('login', function (Request $request) {
        return Limit::perMinute(5)
            ->by($request->input('email') . '|' . $request->ip())
            ->response(function (Request $request, array $headers) {
                return response()->json([
                    'message'    => 'Terlalu banyak percobaan. Coba lagi dalam 1 menit.',
                    'retry_after'=> $headers['Retry-After'],
                ], 429, $headers);
            });
    });
}

// ── PENGGUNAAN DI ROUTE ────────────────────────────────
Route::post('/contact', ContactController::class)
    ->middleware('throttle:contact-form');

Route::middleware('throttle:api')->group(function () {
    Route::apiResource('posts', PostController::class);
});

// Throttle bawaan: X request per Y menit
Route::post('/upload', UploadController::class)
    ->middleware('throttle:10,1'); // 10 request per 1 menit

Gates: Authorization Sederhana

Gates adalah closure-based authorization — cara paling sederhana mendefinisikan apakah seorang user boleh melakukan aksi tertentu. Gate didefinisikan di AppServiceProvider dan bisa digunakan di mana saja dalam aplikasi. Cocok untuk pengecekan authorization yang tidak terikat pada model tertentu — misalnya "apakah user adalah admin?" atau "apakah fitur ini aktif untuk user ini?".

#
Definisikan Gate di AppServiceProvider

Gate didefinisikan menggunakan Gate::define() dengan nama gate sebagai key dan closure yang menerima user sebagai parameter pertama. Closure harus mengembalikan boolean — true berarti diizinkan, false berarti ditolak.

#
Gunakan Gate di Controller

Gunakan Gate::allows() untuk mengecek apakah user memiliki izin, Gate::denies() untuk kebalikannya, atau Gate::authorize() yang otomatis throw AuthorizationException jika tidak diizinkan.

#
Gunakan Gate di Blade

Direktif @can dan @cannot memungkinkan kamu mengecek authorization langsung di template Blade untuk menampilkan atau menyembunyikan elemen UI berdasarkan hak akses user.

PHP
<?php
// app/Providers/AppServiceProvider.php

use Illuminate\Support\Facades\Gate;
use App\Models\{User, Post};

public function boot(): void
{
    // ── GATE SEDERHANA ────────────────────────────────
    // Gate berdasarkan role
    Gate::define('access-admin-panel', function (User $user) {
        return $user->is_admin;
    });

    Gate::define('publish-post', function (User $user) {
        return $user->is_admin || $user->is_author;
    });

    Gate::define('view-analytics', function (User $user) {
        return $user->is_admin || $user->is_premium;
    });

    // ── GATE DENGAN MODEL ─────────────────────────────
    // Parameter kedua adalah model yang sedang diperiksa
    Gate::define('update-post', function (User $user, Post $post) {
        // Admin bisa update semua post
        // Author hanya bisa update post miliknya sendiri
        return $user->is_admin || $user->id === $post->user_id;
    });

    Gate::define('delete-post', function (User $user, Post $post) {
        return $user->is_admin || $user->id === $post->user_id;
    });

    Gate::define('publish-this-post', function (User $user, Post $post) {
        return $user->is_admin ||
               ($user->is_author && $user->id === $post->user_id);
    });

    // ── SUPER ADMIN: Bypass semua gate ────────────────
    Gate::before(function (User $user, string $ability) {
        if ($user->is_superadmin) {
            return true; // true = bypass semua pengecekan
        }
    });

    // ── GATE UNTUK BUKAN ADMIN (jarang dipakai) ───────
    Gate::after(function (User $user, string $ability, ?bool $result) {
        // Dipanggil setelah semua gate lain
        // Bisa override hasil gate sebelumnya
    });
}
PHP
<?php
// ── MENGGUNAKAN GATE DI CONTROLLER ────────────────────

use Illuminate\Support\Facades\Gate;

class PostController extends Controller
{
    public function index()
    {
        // allows() — kembalikan boolean
        if (Gate::allows('access-admin-panel')) {
            // user adalah admin
        }

        // denies() — kebalikan allows()
        if (Gate::denies('publish-post')) {
            abort(403);
        }

        // check() — sama dengan allows()
        $canPublish = Gate::check('publish-post');
    }

    public function update(Request $request, Post $post)
    {
        // authorize() — throw AuthorizationException jika gagal
        // Otomatis return 403 response
        Gate::authorize('update-post', $post);

        // Kode di bawah hanya dijalankan jika authorized
        $post->update($request->validated());
    }

    public function destroy(Post $post)
    {
        // Cara alternatif: gunakan $this->authorize() dari Controller
        $this->authorize('delete-post', $post);

        $post->delete();
        return redirect()->route('posts.index');
    }

    // ── MENGGUNAKAN AUTH USER LANGSUNG ────────────────
    public function publish(Post $post)
    {
        // auth()->user()->can() — pendekatan melalui user instance
        if (auth()->user()->cannot('publish-this-post', $post)) {
            abort(403, 'Kamu tidak berhak mempublikasikan post ini.');
        }

        $post->update([
            'status'       => 'published',
            'published_at' => now(),
        ]);

        return redirect()->route('posts.show', $post)
            ->with('success', 'Post berhasil dipublikasikan!');
    }
}
HTML
{{-- ── MENGGUNAKAN GATE DI BLADE ──────────────────── --}}

{{-- @can: tampilkan jika user MEMILIKI izin --}}
@can('access-admin-panel')
    <a href="{{ route('admin.dashboard') }}"
       class="btn-admin">
        Panel Admin
    </a>
@endcan

{{-- @cannot: tampilkan jika user TIDAK memiliki izin --}}
@cannot('publish-post')
    <div class="upgrade-banner">
        Upgrade akun untuk bisa mempublikasikan artikel.
    </div>
@endcannot

{{-- @can dengan model --}}
@can('update-post', $post)
    <a href="{{ route('posts.edit', $post) }}" class="btn-edit">
        Edit Post
    </a>
@endcan

@can('delete-post', $post)
    <form action="{{ route('posts.destroy', $post) }}" method="POST">
        @csrf
        @method('DELETE')
        <button type="submit" class="btn-danger"
                onclick="return confirm('Yakin hapus?')">
            Hapus Post
        </button>
    </form>
@endcan

@can('publish-this-post', $post)
    @if ($post->status !== 'published')
        <form action="{{ route('posts.publish', $post) }}" method="POST">
            @csrf
            @method('PATCH')
            <button type="submit" class="btn-success">
                Publikasikan
            </button>
        </form>
    @endif
@endcan

{{-- @canany: tampilkan jika punya SALAH SATU izin --}}
@canany(['update-post', 'delete-post'], $post)
    <div class="post-actions-menu">
        {{-- Menu aksi untuk pemilik post atau admin --}}
    </div>
@endcanany

Policies: Authorization Berbasis Model

Gates bagus untuk pengecekan authorization yang sederhana dan tidak terikat model. Tapi saat logika authorization semakin kompleks dan banyak berkaitan dengan model tertentu — seperti semua aksi CRUD pada Post — Policies adalah solusi yang jauh lebih terstruktur. Policy adalah kelas PHP yang mengelompokkan semua logika authorization untuk satu model di satu tempat.

#
Buat Policy dengan Artisan

Artisan membuat file Policy di app/Policies/ dan otomatis mendaftarkannya. Flag --model menghasilkan method yang sesuai dengan tujuh aksi resource (viewAny, view, create, update, delete, restore, forceDelete).

#
Isi Logika di Setiap Method Policy

Setiap method menerima user yang sedang login sebagai parameter pertama dan instance model sebagai parameter kedua (kecuali viewAny dan create yang tidak memerlukan instance). Method harus mengembalikan boolean atau menggunakan Response::allow()/deny() untuk respons dengan pesan custom.

#
Gunakan Policy di Controller dan Blade

Policy bisa digunakan melalui $this->authorize() di Controller, Gate::authorize() dengan nama policy, atau langsung melalui user()->can() dan direktif @can di Blade. Laravel otomatis mencocokkan nama method Gate dengan nama method di Policy.

Bash
# Buat Policy untuk model Post
php artisan make:policy PostPolicy --model=Post

# Buat Policy tanpa otomatis generate method
php artisan make:policy CommentPolicy

# File dibuat di: app/Policies/PostPolicy.php
PHP
<?php
// app/Policies/PostPolicy.php

namespace App\Policies;

use App\Models\Post;
use App\Models\User;
use Illuminate\Auth\Access\Response;

class PostPolicy
{
    /**
     * Opsional: Before hook — dipanggil sebelum method lain
     * Kembalikan true untuk bypass semua cek (super admin)
     * Kembalikan null untuk lanjut ke method yang sesuai
     */
    public function before(User $user, string $ability): bool|null
    {
        if ($user->is_superadmin) {
            return true; // Super admin bisa lakukan segalanya
        }

        return null; // Lanjut ke method policy yang sesuai
    }

    /**
     * Apakah user bisa melihat daftar post?
     * Dipanggil oleh: Gate::allows('viewAny', Post::class)
     */
    public function viewAny(User $user): bool
    {
        // Semua user yang login bisa lihat daftar post
        return true;
    }

    /**
     * Apakah user bisa melihat post tertentu?
     * Dipanggil oleh: Gate::allows('view', $post)
     */
    public function view(?User $user, Post $post): bool
    {
        // Post published bisa dilihat semua orang (termasuk tamu)
        if ($post->status === 'published') {
            return true;
        }

        // Post non-published hanya bisa dilihat pemilik atau admin
        return $user && ($user->id === $post->user_id || $user->is_admin);
    }

    /**
     * Apakah user bisa membuat post baru?
     */
    public function create(User $user): bool
    {
        return $user->is_admin || $user->is_author;
    }

    /**
     * Apakah user bisa mengupdate post tertentu?
     */
    public function update(User $user, Post $post): bool
    {
        // Admin bisa update semua post
        if ($user->is_admin) return true;

        // Author hanya bisa update post miliknya
        return $user->id === $post->user_id;
    }

    /**
     * Apakah user bisa menghapus post tertentu?
     */
    public function delete(User $user, Post $post): bool
    {
        return $user->is_admin || $user->id === $post->user_id;
    }

    /**
     * Apakah user bisa mempublikasikan post?
     * Custom ability di luar 7 method standar
     */
    public function publish(User $user, Post $post): bool
    {
        return $user->is_admin ||
               ($user->is_author && $user->id === $post->user_id);
    }

    /**
     * Apakah user bisa restore post yang di-soft-delete?
     */
    public function restore(User $user, Post $post): bool
    {
        return $user->is_admin;
    }

    /**
     * Apakah user bisa hapus permanen post?
     */
    public function forceDelete(User $user, Post $post): bool
    {
        return $user->is_admin;
    }

    /**
     * Mengembalikan Response dengan pesan kustom
     */
    public function updateWithMessage(User $user, Post $post): Response
    {
        return $user->id === $post->user_id
            ? Response::allow()
            : Response::deny('Kamu hanya bisa mengedit post milikmu sendiri.');
    }
}
PHP
<?php
// ── MENDAFTARKAN POLICY ────────────────────────────────
// Laravel 11+ otomatis discover policy jika mengikuti konvensi naming:
// Model Post → Policy PostPolicy (di app/Policies/)
// Tidak perlu registrasi manual!

// Jika nama tidak mengikuti konvensi, daftarkan di AppServiceProvider:
use Illuminate\Support\Facades\Gate;

public function boot(): void
{
    Gate::policy(Post::class, PostPolicy::class);
    Gate::policy(Comment::class, CommentPolicy::class);
}

// ── MENGGUNAKAN POLICY DI CONTROLLER ─────────────────

class PostController extends Controller
{
    public function index()
    {
        // viewAny: apakah bisa lihat daftar?
        $this->authorize('viewAny', Post::class);

        $posts = Post::published()->paginate(12);
        return view('posts.index', compact('posts'));
    }

    public function show(Post $post)
    {
        // view: apakah bisa lihat post ini?
        $this->authorize('view', $post);

        return view('posts.show', compact('post'));
    }

    public function create()
    {
        // create: apakah bisa buat post baru?
        $this->authorize('create', Post::class);

        $categories = Category::all();
        return view('posts.create', compact('categories'));
    }

    public function store(StorePostRequest $request)
    {
        $this->authorize('create', Post::class);

        $post = Post::create([
            ...$request->validated(),
            'user_id' => auth()->id(),
        ]);

        return redirect()->route('posts.show', $post);
    }

    public function edit(Post $post)
    {
        // update: apakah bisa edit post ini?
        $this->authorize('update', $post);

        $categories = Category::all();
        return view('posts.edit', compact('post', 'categories'));
    }

    public function update(UpdatePostRequest $request, Post $post)
    {
        $this->authorize('update', $post);

        $post->update($request->validated());
        return redirect()->route('posts.show', $post);
    }

    public function destroy(Post $post)
    {
        $this->authorize('delete', $post);

        $post->delete();
        return redirect()->route('posts.index')
            ->with('success', 'Post berhasil dihapus.');
    }

    // Custom ability
    public function publish(Post $post)
    {
        $this->authorize('publish', $post);

        $post->update([
            'status'       => 'published',
            'published_at' => now(),
        ]);

        return redirect()->route('posts.show', $post)
            ->with('success', 'Post berhasil dipublikasikan!');
    }
}
HTML
{{-- ── MENGGUNAKAN POLICY DI BLADE ─────────────────── --}}

{{-- @can dengan nama ability yang sama dengan method Policy --}}
@can('create', App\Models\Post::class)
    <a href="{{ route('posts.create') }}" class="btn-primary">
        + Tulis Artikel Baru
    </a>
@endcan

{{-- @can dengan model instance --}}
@can('update', $post)
    <a href="{{ route('posts.edit', $post) }}" class="btn-outline">
        Edit
    </a>
@endcan

@can('delete', $post)
    <form action="{{ route('posts.destroy', $post) }}" method="POST"
          onsubmit="return confirm('Yakin ingin menghapus?')">
        @csrf
        @method('DELETE')
        <button type="submit" class="btn-danger">Hapus</button>
    </form>
@endcan

@can('publish', $post)
    @if ($post->status !== 'published')
        <form action="{{ route('posts.publish', $post) }}" method="POST">
            @csrf
            @method('PATCH')
            <button type="submit" class="btn-success">
                Publikasikan
            </button>
        </form>
    @endif
@endcan

{{-- Shorthand menggunakan user()->can() di Blade expression --}}
@if (auth()->user()?->can('update', $post))
    {{-- ... --}}
@endif

💡 Gunakan Policy ketika authorization berkaitan erat dengan sebuah Model (Post, Comment, User, dll) karena kode menjadi lebih terorganisir dan mudah di-test. Gunakan Gate ketika authorization bersifat umum dan tidak terikat Model tertentu (misalnya: apakah user adalah admin?, apakah fitur X aktif untuk user ini?). Keduanya bisa digunakan bersama dalam satu aplikasi.


Middleware vs Gate vs Policy: Kapan Menggunakan Masing-Masing?

Sering kali developer bingung kapan harus menggunakan middleware, gate, atau policy karena ketiganya bisa digunakan untuk membatasi akses. Berikut adalah panduan praktis untuk memilih yang tepat berdasarkan konteks kebutuhanmu.

Text
─── KAPAN MENGGUNAKAN MASING-MASING ─────────────────────

MIDDLEWARE — Gunakan untuk:
✓ Pengecekan SEBELUM request sampai ke Controller
✓ Kondisi yang berlaku untuk BANYAK route sekaligus
✓ Hal-hal yang tidak berkaitan dengan model spesifik
✓ Pengecekan berdasarkan request itu sendiri (header, IP, dll)

Contoh penggunaan tepat:
→ Pastikan user sudah login (middleware 'auth')
→ Pastikan user punya role tertentu (middleware 'role:admin')
→ Rate limiting endpoint (middleware 'throttle')
→ Pastikan email sudah diverifikasi (middleware 'verified')
→ Pastikan akun masih aktif (middleware 'active')
→ Tambahkan security headers ke semua response

GATE — Gunakan untuk:
✓ Pengecekan di DALAM Controller atau Service
✓ Authorization yang tidak terikat model spesifik
✓ Kondisi sederhana yang cukup didefinisikan sebagai closure
✓ Fitur-fitur yang berdasarkan role atau status user

Contoh penggunaan tepat:
→ Apakah user bisa akses panel admin?
→ Apakah user punya fitur premium?
→ Apakah user bisa publish post (role-based, bukan per-post)?
→ Apakah mode maintenance aktif?

POLICY — Gunakan untuk:
✓ Authorization yang BERKAITAN ERAT dengan model tertentu
✓ Banyak aksi berbeda pada model yang sama
✓ Logika authorization yang kompleks per-instance
✓ Kode yang ingin bisa di-test secara independen

Contoh penggunaan tepat:
→ Apakah user bisa edit POST tertentu? (berdasarkan ownership)
→ Apakah user bisa delete COMMENT tertentu?
→ Apakah user bisa approve ORDER tertentu?
→ Semua aksi CRUD pada satu model

─── SUMMARY ──────────────────────────────────────────────

Request masuk
    ↓
Middleware (filter request-level)
    ↓
Controller
    ↓
Gate / Policy (filter action-level)
    ↓
Response keluar
    ↑
Middleware (bisa modifikasi response)

Praktik: Sistem Authorization Lengkap untuk Blog

Mari satukan semua konsep yang sudah dipelajari — middleware, gate, dan policy — dalam implementasi authorization yang lengkap untuk aplikasi blog. Ini mensimulasikan pola authorization yang akan kamu temui di proyek nyata dengan tiga level akses: pengunjung, penulis, dan admin.

PHP
<?php
// app/Providers/AppServiceProvider.php — Setup Gates

use Illuminate\Support\Facades\Gate;

public function boot(): void
{
    // Super admin bypass semua pengecekan
    Gate::before(function (User $user, string $ability) {
        if ($user->is_superadmin) return true;
    });

    // Gates untuk fitur umum (bukan per-model)
    Gate::define('access-admin', fn(User $user) => $user->is_admin);
    Gate::define('write-article', fn(User $user) => $user->is_admin || $user->is_author);
    Gate::define('moderate-comments', fn(User $user) => $user->is_admin || $user->is_moderator);
    Gate::define('view-analytics', fn(User $user) => $user->is_admin);
    Gate::define('manage-users', fn(User $user) => $user->is_admin);
}
PHP
<?php
// routes/web.php — Struktur route dengan authorization berlapis

use Illuminate\Support\Facades\Route;

// ── PUBLIK: Siapapun bisa akses ────────────────────────
Route::get('/', [HomeController::class, 'index'])->name('home');
Route::get('/blog', [PostController::class, 'index'])->name('blog.index');
Route::get('/blog/{post:slug}', [PostController::class, 'show'])->name('blog.show');
Route::get('/kategori/{category:slug}', [CategoryController::class, 'show'])->name('category.show');

// ── GUEST ONLY: Hanya tamu ────────────────────────────
Route::middleware('guest')->group(function () {
    // Route login, register, dll (dari auth.php)
});

// ── AUTH: Harus login ─────────────────────────────────
Route::middleware(['auth', 'active'])->group(function () {
    Route::get('/dashboard', [DashboardController::class, 'index'])->name('dashboard');

    // Profile (semua user)
    Route::controller(ProfileController::class)->group(function () {
        Route::get('/profile', 'edit')->name('profile.edit');
        Route::patch('/profile', 'update')->name('profile.update');
        Route::delete('/profile', 'destroy')->name('profile.destroy');
    });

    // Komentar (semua user yang login)
    Route::post('/posts/{post}/comments', [CommentController::class, 'store'])
        ->name('comments.store');
    Route::delete('/comments/{comment}', [CommentController::class, 'destroy'])
        ->name('comments.destroy');
});

// ── AUTHOR: Harus punya role author/admin ─────────────
Route::middleware(['auth', 'active', 'role:author,admin'])->group(function () {
    Route::get('/my-posts', [AuthorController::class, 'posts'])->name('author.posts');
    Route::resource('posts', PostController::class)->except(['index', 'show']);
    Route::patch('/posts/{post}/publish', [PostController::class, 'publish'])
        ->name('posts.publish');
    Route::patch('/posts/{post}/unpublish', [PostController::class, 'unpublish'])
        ->name('posts.unpublish');
});

// ── ADMIN: Harus role admin ────────────────────────────
Route::middleware(['auth', 'active', 'role:admin'])
    ->prefix('admin')
    ->name('admin.')
    ->group(function () {
        Route::get('/', [Admin\DashboardController::class, 'index'])->name('dashboard');
        Route::resource('users', Admin\UserController::class);
        Route::resource('categories', Admin\CategoryController::class);
        Route::get('analytics', [Admin\AnalyticsController::class, 'index'])->name('analytics');
        Route::post('comments/{comment}/approve', [Admin\CommentController::class, 'approve'])
            ->name('comments.approve');
    });

require __DIR__ . '/auth.php';
PHP
<?php
// app/Http/Controllers/PostController.php — Dengan Policy

namespace App\Http\Controllers;

use App\Models\Post;
use App\Http\Requests\{StorePostRequest, UpdatePostRequest};
use Illuminate\Support\Facades\DB;
use Illuminate\View\View;
use Illuminate\Http\RedirectResponse;

class PostController extends Controller
{
    public function index(): View
    {
        // Tampilan berbeda untuk admin vs user biasa
        $posts = auth()->user()?->can('access-admin')
            ? Post::with(['user', 'category'])->latest()->paginate(20)
            : Post::published()->with(['user', 'category'])->latest()->paginate(12);

        return view('posts.index', compact('posts'));
    }

    public function show(Post $post): View
    {
        // Policy view: published bisa dilihat semua orang,
        // draft hanya pemilik/admin
        $this->authorize('view', $post);

        $post->load(['user.profile', 'category', 'tags', 'approvedComments.user']);
        $post->increment('views');

        return view('posts.show', compact('post'));
    }

    public function create(): View
    {
        $this->authorize('create', Post::class); // Policy create

        return view('posts.create', [
            'categories' => Category::all(),
            'tags'       => Tag::all(),
        ]);
    }

    public function store(StorePostRequest $request): RedirectResponse
    {
        $this->authorize('create', Post::class);

        $post = DB::transaction(fn() => tap(
            Post::create([
                ...$request->validated(),
                'user_id'      => auth()->id(),
                'published_at' => $request->status === 'published' ? now() : null,
            ]),
            fn($post) => $post->tags()->sync($request->tags ?? [])
        ));

        return redirect()->route('posts.show', $post)
            ->with('success', 'Artikel berhasil disimpan!');
    }

    public function edit(Post $post): View
    {
        $this->authorize('update', $post); // Policy update

        return view('posts.edit', [
            'post'       => $post,
            'categories' => Category::all(),
            'tags'       => Tag::all(),
        ]);
    }

    public function update(UpdatePostRequest $request, Post $post): RedirectResponse
    {
        $this->authorize('update', $post);

        $post->update($request->validated());
        $post->tags()->sync($request->tags ?? []);

        return redirect()->route('posts.show', $post)
            ->with('success', 'Artikel berhasil diperbarui!');
    }

    public function destroy(Post $post): RedirectResponse
    {
        $this->authorize('delete', $post); // Policy delete

        $post->delete();

        return redirect()->route('posts.index')
            ->with('success', 'Artikel berhasil dihapus.');
    }

    public function publish(Post $post): RedirectResponse
    {
        $this->authorize('publish', $post); // Custom policy ability

        $post->update([
            'status'       => 'published',
            'published_at' => now(),
        ]);

        return redirect()->route('posts.show', $post)
            ->with('success', 'Artikel berhasil dipublikasikan!');
    }
}

⚠️ Jangan pernah mengandalkan hanya tampilan UI untuk keamanan — misalnya hanya menyembunyikan tombol "Hapus" di Blade tanpa pengecekan authorization di Controller. Pengguna yang iseng bisa langsung mengirim request DELETE via Postman atau curl tanpa melalui UI. Selalu lakukan pengecekan authorization di sisi server (Controller/Policy/Middleware) dan anggap pengecekan di Blade hanya sebagai kosmetik untuk UX yang lebih baik.


Langkah Selanjutnya

Luar biasa — kamu sudah menguasai Middleware dan Authorization di Laravel secara menyeluruh! Mulai dari membuat middleware kustom, rate limiting yang fleksibel, mendefinisikan Gates untuk pengecekan sederhana, membuat Policies yang terstruktur untuk authorization berbasis Model, hingga membangun sistem authorization berlapis yang lengkap untuk aplikasi blog. Di bagian kesebelas seri ini, kita akan membahas File Upload dan Storage — cara mengelola file yang di-upload pengguna dengan aman, termasuk image resizing, penyimpanan di berbagai disk (local, S3, Cloudflare R2), dan best practices untuk manajemen file di production.

🚀 Sebelum lanjut ke Bagian 11, pastikan kamu sudah berlatih: (1) membuat minimal satu middleware kustom dan menerapkannya ke route group, (2) mendefinisikan beberapa Gate di AppServiceProvider dan menggunakannya di Controller dan Blade, (3) membuat PostPolicy dengan semua method CRUD dan menghubungkannya ke PostController dengan $this->authorize(), dan (4) memastikan authorization di Controller selalu ada meski elemen UI sudah disembunyikan di Blade. Authorization yang benar adalah salah satu aspek paling penting dalam keamanan aplikasi web.