Laravel Middleware Uitgelicht: Authenticatie, Rate Limiting en Eigen Middleware

Uitgebreide handleiding over Laravel middleware met praktische voorbeelden voor authenticatie, rate limiting met throttle, eigen middleware bouwen en geavanceerde patronen voor productieomgevingen.

Laravel middleware-architectuur met request-pipeline, authenticatie en rate limiting

Middleware vormt een van de meest krachtige mechanismen binnen het Laravel-framework. Elk HTTP-verzoek dat een Laravel-applicatie bereikt, passeert een reeks middleware-lagen voordat het de bijbehorende controller bereikt. Deze lagen maken het mogelijk om authenticatie af te dwingen, verzoeken te beperken, invoer te valideren en beveiligingsheaders toe te voegen, zonder dat de controller zelf van deze verantwoordelijkheden op de hoogte hoeft te zijn. Dit artikel biedt een diepgaande verkenning van de middleware-pipeline in Laravel, van het ingebouwde authenticatie- en rate limiting-systeem tot het bouwen en registreren van volledig eigen middleware.

Vereiste kennis

Dit artikel gaat ervan uit dat de lezer beschikt over basiskennis van Laravel-routing en de service container. Ervaring met het opzetten van authenticatie via Laravel Breeze of Fortify is nuttig, maar niet strikt noodzakelijk. De codevoorbeelden zijn gebaseerd op Laravel 11 en 12, die gebruikmaken van de vereenvoudigde middlewareconfiguratie in bootstrap/app.php.

Hoe de middleware-pipeline werkt

De middleware-pipeline van Laravel volgt het principe van een "ui-structuur" (onion architecture). Wanneer een HTTP-verzoek binnenkomt, passeert het achtereenvolgens elke middleware-laag, bereikt vervolgens de kern -- de controller -- en keert daarna terug door dezelfde lagen in omgekeerde volgorde. Dit tweerichtingsverkeer maakt het mogelijk om logica zowel voor als na de verwerking van het verzoek uit te voeren.

Een concreet voorbeeld verduidelijkt dit patroon. De volgende middleware meet de totale verwerkingstijd van een verzoek door een tijdstempel vast te leggen voordat het verzoek wordt doorgestuurd, en de verstreken tijd te loggen nadat het antwoord is gegenereerd:

app/Http/Middleware/LogRequestTime.phpphp
namespace App\Http\Middleware;

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

class LogRequestTime
{
    public function handle(Request $request, Closure $next): Response
    {
        $start = microtime(true);          // Capture start time

        $response = $next($request);       // Pass to next middleware

        $duration = microtime(true) - $start;
        Log::info('Request completed', [
            'url'      => $request->url(),
            'method'   => $request->method(),
            'duration' => round($duration * 1000, 2) . 'ms',
        ]);

        return $response;                  // Return response up the stack
    }
}

De $next($request)-aanroep vormt het scharnierpunt. Alle code die ervoor staat, wordt uitgevoerd op weg naar de controller (het "inkomende" gedeelte). Alle code die erna staat, wordt uitgevoerd op de terugweg, wanneer het antwoord al beschikbaar is. Dit maakt het mogelijk om in een enkele middleware zowel het verzoek als het antwoord te inspecteren of te wijzigen.

Authenticatiemiddleware

Authenticatie is verreweg de meest toegepaste middleware in Laravel-applicaties. Het framework biedt de auth-middleware die controleert of een gebruiker is ingelogd en, indien niet, het verzoek omleidt naar de inlogpagina of een 401-statuscode retourneert.

Het beschermen van routes met authenticatiemiddleware kan op twee manieren: per individuele route of via een routegroep die meerdere routes tegelijk afschermt.

routes/web.phpphp
use App\Http\Controllers\DashboardController;
use App\Http\Controllers\ProfileController;

// Single route protection
Route::get('/dashboard', [DashboardController::class, 'index'])
    ->middleware('auth');

// Group protection for multiple routes
Route::middleware('auth')->group(function () {
    Route::get('/profile', [ProfileController::class, 'show']);
    Route::put('/profile', [ProfileController::class, 'update']);
    Route::delete('/profile', [ProfileController::class, 'destroy']);
});

De groepsaanpak verdient de voorkeur wanneer meerdere routes dezelfde bescherming nodig hebben. Het voorkomt herhaling en maakt de routestructuur overzichtelijker.

Meerdere guards gebruiken

Laravel ondersteunt meerdere authenticatieguards, waarmee verschillende delen van de applicatie verschillende authenticatiemechanismen kunnen hanteren. API-routes maken doorgaans gebruik van tokengebaseerde authenticatie via Sanctum, terwijl het beheerderspaneel een aparte guard kan gebruiken met strengere toegangscontrole.

routes/api.phpphp
// API routes use the 'sanctum' guard
Route::middleware('auth:sanctum')->group(function () {
    Route::get('/user', fn (Request $request) => $request->user());
    Route::apiResource('/orders', OrderController::class);
});

// routes/web.php
// Admin routes use a custom 'admin' guard
Route::middleware('auth:admin')->prefix('admin')->group(function () {
    Route::get('/dashboard', [AdminController::class, 'index']);
    Route::get('/users', [AdminController::class, 'users']);
});

De guard wordt opgegeven als parameter na de dubbele punt in de middleware-naam. Laravel controleert vervolgens de authenticatiestatus via de opgegeven guard in plaats van de standaard webguard.

De guest-middleware

Naast auth biedt Laravel ook de guest-middleware, die precies het omgekeerde doet: alleen niet-ingelogde gebruikers mogen de route bereiken. Dit is bijzonder nuttig voor login- en registratiepagina's. Een ingelogde gebruiker die naar /login navigeert, wordt automatisch doorgestuurd naar het dashboard in plaats van het inlogformulier opnieuw te zien.

Rate limiting met throttle

Rate limiting beschermt een applicatie tegen misbruik door het aantal verzoeken per tijdseenheid te beperken. Laravel biedt hiervoor de throttle-middleware, die standaard het aantal verzoeken per minuut begrenst op basis van het IP-adres of de geauthenticeerde gebruiker.

De eenvoudigste configuratie specificeert het maximale aantal verzoeken en de tijdsperiode in minuten:

routes/api.phpphp
// Allow 60 requests per minute per user
Route::middleware('throttle:60,1')->group(function () {
    Route::get('/posts', [PostController::class, 'index']);
    Route::get('/posts/{post}', [PostController::class, 'show']);
});

// Stricter limit for write operations
Route::middleware(['auth:sanctum', 'throttle:10,1'])->group(function () {
    Route::post('/posts', [PostController::class, 'store']);
    Route::put('/posts/{post}', [PostController::class, 'update']);
});

Het onderscheid tussen lees- en schrijfoperaties is een gangbare praktijk. Leesoperaties zoals het ophalen van berichten kunnen een hogere limiet verdragen, terwijl schrijfoperaties strenger worden beperkt om misbruik te voorkomen.

Benoemde rate limiters

Voor geavanceerdere scenario's biedt de RateLimiter-facade de mogelijkheid om benoemde limiters te definieren met dynamische logica. Hiermee kunnen limieten worden afgestemd op het type gebruiker, het abonnementsniveau of andere criteria.

app/Providers/AppServiceProvider.phpphp
use Illuminate\Cache\RateLimiting\Limit;
use Illuminate\Support\Facades\RateLimiter;
use Illuminate\Http\Request;

public function boot(): void
{
    // API rate limiter with tiered access
    RateLimiter::for('api', function (Request $request) {
        $user = $request->user();

        if ($user?->hasSubscription('enterprise')) {
            return Limit::perMinute(500)->by($user->id);   // Enterprise: 500/min
        }

        if ($user) {
            return Limit::perMinute(100)->by($user->id);   // Authenticated: 100/min
        }

        return Limit::perMinute(20)->by($request->ip());   // Anonymous: 20/min
    });

    // Login limiter to prevent brute force
    RateLimiter::for('login', function (Request $request) {
        return Limit::perMinute(5)
            ->by($request->ip())                            // Key by IP address
            ->response(function () {                        // Custom exceeded response
                return response()->json([
                    'message' => 'Too many login attempts. Try again in a minute.',
                ], 429);
            });
    });
}

De api-limiter in het bovenstaande voorbeeld past een gelaagd systeem toe: enterprisegebruikers krijgen 500 verzoeken per minuut, reguliere geauthenticeerde gebruikers 100, en anonieme bezoekers slechts 20. De login-limiter beperkt inlogpogingen tot vijf per minuut per IP-adres en retourneert een aangepaste JSON-foutmelding wanneer de limiet wordt overschreden.

Het toepassen van deze benoemde limiters op routes is eenvoudig:

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

// routes/web.php
Route::middleware('throttle:login')
    ->post('/login', [AuthController::class, 'login']);

In plaats van harde getallen worden nu de namen van de gedefinieerde limiters gebruikt. Dit maakt de routedefinities leesbaarder en centraliseert de rate limiting-logica op een enkele plek.

Klaar om je Laravel gesprekken te halen?

Oefen met onze interactieve simulatoren, flashcards en technische tests.

Eigen middleware bouwen

Naast de ingebouwde middleware biedt Laravel volledige vrijheid om eigen middleware te ontwikkelen voor applicatiespecifieke logica. Het aanmaken van een nieuw middlewarebestand gebeurt via het Artisan-commando:

bash
php artisan make:middleware EnsureUserHasRole

Dit genereert een skelet in app/Http/Middleware/ met de vereiste handle-methode. Van daaruit kan de middleware naar wens worden ingevuld.

Rolgebaseerde middleware

Een veelvoorkomend gebruik van eigen middleware is autorisatie op basis van gebruikersrollen. De volgende middleware accepteert een variabel aantal rollen als parameters en blokkeert de toegang wanneer de ingelogde gebruiker geen van de vereiste rollen bezit:

app/Http/Middleware/EnsureUserHasRole.phpphp
namespace App\Http\Middleware;

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

class EnsureUserHasRole
{
    public function handle(Request $request, Closure $next, string ...$roles): Response
    {
        $user = $request->user();

        if (! $user || ! $user->hasAnyRole($roles)) {
            abort(403, 'Insufficient permissions.');
        }

        return $next($request);
    }
}

De ...$roles-parameter (variadic) maakt het mogelijk om meerdere rollen op te geven via de routedefinitie, gescheiden door komma's. De middleware controleert of de gebruiker tenminste een van de opgegeven rollen heeft.

De registratie en het gebruik van deze middleware ziet er als volgt uit:

bootstrap/app.phpphp
->withMiddleware(function (Middleware $middleware) {
    $middleware->alias([
        'role' => \App\Http\Middleware\EnsureUserHasRole::class,
    ]);
})

// routes/web.php
Route::middleware('role:admin')->group(function () {
    Route::get('/admin', [AdminController::class, 'index']);
});

// Multiple roles: admin OR editor can access
Route::middleware('role:admin,editor')->group(function () {
    Route::resource('/articles', ArticleController::class);
});

API-verzoeksanering

Een ander praktisch voorbeeld is middleware die binnenkomende API-verzoeken valideert en opschoont. De volgende middleware wijst niet-JSON-verzoeken af en verwijdert overtollige witruimte uit alle invoervelden:

app/Http/Middleware/ApiRequestSanitizer.phpphp
namespace App\Http\Middleware;

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

class ApiRequestSanitizer
{
    public function handle(Request $request, Closure $next): Response
    {
        // Reject non-JSON requests on API routes
        if (! $request->expectsJson() && $request->isMethod('POST')) {
            return response()->json(
                ['error' => 'Content-Type must be application/json'],
                415
            );
        }

        // Trim all string inputs
        $input = $request->all();
        array_walk_recursive($input, function (&$value) {
            if (is_string($value)) {
                $value = trim($value);
            }
        });
        $request->merge($input);

        return $next($request);
    }
}

Deze middleware combineert twee verantwoordelijkheden die samen een zinvolle eenheid vormen: het afdwingen van het juiste inhoudstype en het normaliseren van de invoer. Door dit als middleware te implementeren, hoeven individuele controllers zich niet met deze controles bezig te houden.

Middlewareregistratie in Laravel 12

Sinds Laravel 11 is de middlewareregistratie verplaatst van de traditionele Kernel.php naar bootstrap/app.php. Deze vereenvoudigde aanpak bundelt alle middlewareconfiguratie op een enkele, overzichtelijke locatie.

bootstrap/app.phpphp
use Illuminate\Foundation\Application;
use Illuminate\Foundation\Configuration\Middleware;

return Application::configure(basePath: dirname(__DIR__))
    ->withMiddleware(function (Middleware $middleware) {
        // Global middleware (runs on every request)
        $middleware->append(
            \App\Http\Middleware\LogRequestTime::class
        );

        // Add to the 'web' middleware group
        $middleware->web(append: [
            \App\Http\Middleware\TrackPageViews::class,
        ]);

        // Add to the 'api' middleware group
        $middleware->api(prepend: [
            \App\Http\Middleware\ApiRequestSanitizer::class,
        ]);

        // Register aliases for route-level use
        $middleware->alias([
            'role'       => \App\Http\Middleware\EnsureUserHasRole::class,
            'subscribed' => \App\Http\Middleware\EnsureUserIsSubscribed::class,
        ]);

        // Control execution order
        $middleware->priority([
            \Illuminate\Session\Middleware\StartSession::class,
            \Illuminate\Auth\Middleware\Authenticate::class,
            \App\Http\Middleware\EnsureUserHasRole::class,
        ]);
    })
    ->create();

De configuratie onderscheidt vier niveaus. Globale middleware via append() wordt bij elk verzoek uitgevoerd. Groepsmiddleware via web() en api() geldt alleen voor routes binnen de betreffende groep. Aliassen via alias() registreren middleware onder een korte naam voor gebruik in routedefinities. De prioriteitslijst via priority() bepaalt de volgorde waarin middleware wordt uitgevoerd, ongeacht de volgorde waarin ze zijn geregistreerd.

Uitvoeringsvolgorde is cruciaal

De volgorde van middleware-uitvoering kan subtiele maar ernstige fouten veroorzaken. Authenticatiemiddleware moet altijd worden uitgevoerd nadat de sessie is gestart, anders is er geen sessie-informatie beschikbaar om de gebruiker te identificeren. Rolgebaseerde middleware moet na authenticatie worden uitgevoerd, omdat anders de gebruiker nog niet bekend is. De priority()-methode garandeert deze volgorde, zelfs wanneer middleware in een andere volgorde op routes wordt toegepast.

Terminable middleware

Standaard voert middleware logica uit voordat het antwoord naar de client wordt verzonden. Terminable middleware biedt echter de mogelijkheid om werk uit te voeren nadat het antwoord al is verzonden. Dit is ideaal voor taken die de eindgebruiker niet hoeft af te wachten, zoals het vastleggen van analytische gegevens of het bijwerken van statistieken.

Een middleware die analytische gegevens verzamelt zonder de responstijd te beinvloeden:

app/Http/Middleware/CollectAnalytics.phpphp
namespace App\Http\Middleware;

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

class CollectAnalytics
{
    public function handle(Request $request, Closure $next): Response
    {
        return $next($request);  // Pass through without delay
    }

    public function terminate(Request $request, Response $response): void
    {
        // Runs after response is sent to client
        DB::table('analytics')->insert([
            'path'        => $request->path(),
            'method'      => $request->method(),
            'status_code' => $response->getStatusCode(),
            'user_id'     => $request->user()?->id,
            'ip'          => $request->ip(),
            'created_at'  => now(),
        ]);
    }
}

De handle-methode stuurt het verzoek direct door zonder vertraging. De terminate-methode wordt pas aangeroepen nadat het antwoord volledig naar de browser is verzonden. De database-insert in terminate voegt dus geen milliseconde toe aan de wachttijd van de gebruiker. Dit patroon is bijzonder waardevol voor logging, analytics en elke vorm van achtergrondregistratie die niet tijdkritisch is.

Productiepatronen

In productieomgevingen komen bepaalde middleware-patronen regelmatig terug. Twee van de meest voorkomende zijn onderhoudsmodus-bypass en beveiligingsheaders.

Onderhoudsmodus-bypass

Wanneer een applicatie in onderhoudsmodus staat, moeten ontwikkelaars en beheerders vaak wel toegang behouden. De volgende middleware staat verzoeken vanuit specifieke IP-adressen toe, zelfs tijdens onderhoud:

app/Http/Middleware/MaintenanceBypass.phpphp
class MaintenanceBypass
{
    private array $allowedIps = ['192.168.1.0/24', '10.0.0.1'];

    public function handle(Request $request, Closure $next): Response
    {
        if (app()->isDownForMaintenance()) {
            foreach ($this->allowedIps as $ip) {
                if ($request->ip() === $ip) {
                    return $next($request);
                }
            }
        }

        return $next($request);
    }
}

In een productieomgeving verdient het de aanbeveling om de toegestane IP-adressen via een omgevingsvariabele te configureren in plaats van ze hardcoded in de middleware op te nemen. Dit maakt aanpassingen mogelijk zonder codewijzigingen.

Beveiligingsheaders

Het toevoegen van beveiligingsheaders aan elke response is een eenvoudige maar effectieve maatregel tegen veelvoorkomende webkwetsbaarheden. De volgende middleware voegt een reeks standaard beveiligingsheaders toe:

app/Http/Middleware/SecurityHeaders.phpphp
class SecurityHeaders
{
    public function handle(Request $request, Closure $next): Response
    {
        $response = $next($request);

        $response->headers->set('X-Content-Type-Options', 'nosniff');
        $response->headers->set('X-Frame-Options', 'SAMEORIGIN');
        $response->headers->set('Referrer-Policy', 'strict-origin-when-cross-origin');
        $response->headers->set(
            'Strict-Transport-Security',
            'max-age=31536000; includeSubDomains'
        );

        return $response;
    }
}

De header X-Content-Type-Options: nosniff voorkomt dat browsers het inhoudstype van een response raden, wat MIME-type-aanvallen tegengaat. X-Frame-Options: SAMEORIGIN beschermt tegen clickjacking door het inbedden van de pagina in frames van andere domeinen te blokkeren. Strict-Transport-Security dwingt HTTPS af voor alle toekomstige verzoeken gedurende een jaar. Deze middleware wordt doorgaans als globale middleware geregistreerd zodat elke response deze headers bevat.

Klaar om je Laravel gesprekken te halen?

Oefen met onze interactieve simulatoren, flashcards en technische tests.

Conclusie

Middleware vormt het fundament van verzoekverwerking in Laravel en biedt een elegant mechanisme om doorsnijdende belangen los te koppelen van de kernlogica van een applicatie. De belangrijkste inzichten uit dit artikel:

  • De middleware-pipeline volgt het ui-model: verzoeken bewegen naar binnen door elke laag, bereiken de controller, en antwoorden keren terug door dezelfde lagen in omgekeerde volgorde
  • Authenticatiemiddleware (auth, auth:sanctum, auth:admin) beschermt routes tegen ongeautoriseerde toegang, met ondersteuning voor meerdere guards die elk hun eigen authenticatiemechanisme hanteren
  • Rate limiting via de throttle-middleware en de RateLimiter-facade biedt flexibele configuratie op basis van gebruikerstype, abonnementsniveau of IP-adres, inclusief aangepaste foutmeldingen
  • Eigen middleware wordt aangemaakt via php artisan make:middleware en kan logica uitvoeren voor de controller, na de controller of na het verzenden van het antwoord via de terminate-methode
  • Middlewareregistratie in Laravel 12 vindt plaats in bootstrap/app.php, waar globale middleware, groepsmiddleware, aliassen en uitvoeringsprioriteit centraal worden geconfigureerd
  • Terminable middleware maakt het mogelijk om niet-tijdkritieke taken zoals analytics en logging uit te voeren zonder de responstijd te beinvloeden
  • Productiepatronen zoals onderhoudsmodus-bypass en beveiligingsheaders behoren tot de standaarduitrusting van elke professionele Laravel-applicatie

Begin met oefenen!

Test je kennis met onze gespreksimulatoren en technische tests.

Tags

#laravel
#middleware
#authenticatie
#rate-limiting
#php

Delen

Gerelateerde artikelen