404 Not Found

404 Not Found


nginx

An In-Depth Analysis of Laravel Middleware

Middleware is Laravel's "security checkpoint pipeline"—each request passes through layer upon layer of checks, just like luggage; those that fail are intercepted immediately, while those that pass are allowed to proceed to the next stage.

1. What You'll Learn


2. A True Story of an Architect

(1) Pain Point: All security checks are concentrated in the controller

Bob wrote 10 lines of validation code at the beginning of each controller method—authentication checks, tenant isolation, rate limiting, CORS, and logging. 50 methods x 10 lines = 500 lines of duplicate code. When Alice added a new security policy, she had to make changes in 50 places; she missed three, which led to a security vulnerability. Charlie asked, "Have you guys heard of middleware?"

(2) The Middleware Solution

Middleware extracts common validation logic into separate classes, and each request automatically passes through multiple layers of middleware—authentication, rate limiting, CORS, and tenant isolation—while controllers focus solely on business logic.

PHP
// Before — 10 lines of checks in every method
public function index() {
    if (!auth()->check()) abort(401);
    if (!tenant()->isActive()) abort(403);
    if (RateLimiter::tooManyAttempts(...)) abort(429);
    // ... finally, business logic
}

// After — middleware handles all checks
Route::middleware(['auth', 'tenant.resolve', 'throttle:60,1'])
    ->get('/shops', [ShopController::class, 'index']);
// Controller only has business logic

(3) Revenue

After Bob implemented middleware, his controller code was reduced by 60%; Alice's new security policy required modifying only one piece of middleware, with zero oversights.


3. Middleware Pipeline Execution Flow

(1) The Entire Process of a Request Passing Through the Middleware

100%
flowchart LR
    A[Request] --> B[Global MW 1]
    B --> C[Global MW 2]
    C --> D[Route MW 1]
    D --> E[Route MW 2]
    E --> F[Controller]
    F --> G[Response]
    G --> H[After MW 2]
    H --> I[After MW 1]
    I --> J[Client]

(2) The Onion Model

TEXT
Request →
  Middleware A (before) →
    Middleware B (before) →
      Controller → Response
    Middleware B (after) →
  Middleware A (after) →
Response

Each middleware can:

(1) ▶ Example: Understanding the Onion Model of Middleware

PHP
// app/Http/Middleware/LogRequests.php
class LogRequests
{
    public function handle(Request $request, Closure $next): Response
    {
        // BEFORE — log incoming request
        Log::info('Request:', [
            'method' => $request->method(),
            'url' => $request->fullUrl(),
            'ip' => $request->ip(),
        ]);

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

        // AFTER — log response status
        Log::info('Response:', [
            'status' => $response->getStatusCode(),
            'duration' => defined('LARAVEL_START') ? round((microtime(true) - LARAVEL_START) * 1000) : null,
        ]);

        return $response;
    }
}

Output:

TEXT
// Execution Successful

4. Three Ways to Register

(1) Global Middleware

Every request is executed; there is no need to specify this in the route.

PHP
// bootstrap/app.php
->withMiddleware(function (Middleware $middleware) {
    $middleware->append([
        \App\Http\Middleware\LogRequests::class,
        \App\Http\Middleware\SetLocale::class,
    ]);

    $middleware->remove([
        \Illuminate\Foundation\Http\Middleware\TrimStrings::class,
    ]);
})

(2) Routing Middleware

Middleware specified in the route definition.

PHP
// Register alias
->withMiddleware(function (Middleware $middleware) {
    $middleware->alias([
        'tenant' => \App\Http\Middleware\TenantResolve::class,
        'role' => \App\Http\Middleware\CheckRole::class,
        'ability' => \App\Http\Middleware\CheckAbility::class,
    ]);
})

// Use in routes
Route::middleware(['auth', 'tenant', 'role:admin'])
    ->get('/admin/users', [AdminController::class, 'users']);

(3) Middleware Group

Package a set of middleware and apply it as a group.

Group Name Default Inclusions Applicable To
web StartSession, EncryptCookies, VerifyCsrfToken Web Routing
api Throttle:api, SubstitueBindings API Routing
PHP
// Customize middleware groups
->withMiddleware(function (Middleware $middleware) {
    $middleware->appendToGroup('api', [
        \App\Http\Middleware\TenantResolve::class,
    ]);

    $middleware->prependToGroup('web', [
        \App\Http\Middleware\SetLocale::class,
    ]);
})
Registration Method Timing Applicable Scenarios
Global All Requests Logs, CORS
Route Alias Specified Route Authentication, Tenant
Middleware Group Intra-group Routing Web/API Group

(1) ▶ Example: ShopMetrics Middleware Registration

PHP
// bootstrap/app.php
->withMiddleware(function (Middleware $middleware) {
    // Global middleware
    $middleware->append([
        \App\Http\Middleware\SetLocale::class,
    ]);

    // Aliases
    $middleware->alias([
        'tenant' => \App\Http\Middleware\TenantResolve::class,
        'role' => \App\Http\Middleware\CheckRole::class,
        'ability' => \App\Http\Middleware\CheckAbility::class,
    ]);

    // Add to API group
    $middleware->appendToGroup('api', [
        \App\Http\Middleware\EnsureJsonAccept::class,
    ]);
})

Output:

TEXT
// Execution Successful

5. Custom Middleware

(1) Create middleware

BASH
php artisan make:middleware TenantResolve
php artisan make:middleware CheckRole
php artisan make:middleware EnsureJsonAccept

(2) TenantResolve Middleware

PHP
// app/Http/Middleware/TenantResolve.php
class TenantResolve
{
    public function handle(Request $request, Closure $next): Response
    {
        $user = $request->user();
        if (!$user || !$user->tenant_id) {
            abort(403, 'No tenant associated with this account.');
        }

        $tenant = $user->tenant;

        if ($tenant->status !== 'active') {
            abort(403, 'Tenant account is ' . $tenant->status . '.');
        }

        // Store tenant in container for global scope
        app()->instance(Tenant::class, $tenant);

        // Set tenant context on request
        $request->attributes->set('tenant', $tenant);

        return $next($request);
    }
}

(3) CORS Middleware

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

        $response->headers->set('Access-Control-Allow-Origin', config('cors.allowed_origins'));
        $response->headers->set('Access-Control-Allow-Methods', 'GET,POST,PUT,PATCH,DELETE,OPTIONS');
        $response->headers->set('Access-Control-Allow-Headers', 'Content-Type,Authorization,X-Tenant-Id');
        $response->headers->set('Access-Control-Max-Age', '86400');

        if ($request->isMethod('OPTIONS')) {
            $response->setStatusCode(204);
        }

        return $response;
    }
}

(1) ▶ Example: ShopMetrics Rate-Limiting Middleware

PHP
// app/Http/Middleware/PerTenantRateLimit.php
class PerTenantRateLimit
{
    public function handle(Request $request, Closure $next, int $maxAttempts = 60, int $decayMinutes = 1): Response
    {
        $tenant = app()->make(Tenant::class);
        $key = 'tenant:' . $tenant->id . ':' . $request->ip();

        if (RateLimiter::tooManyAttempts($key, $maxAttempts)) {
            $seconds = RateLimiter::availableIn($key);
            abort(429, "Too many requests. Try again in {$seconds} seconds.");
        }

        RateLimiter::hit($key, $decayMinutes * 60);

        $response = $next($request);

        $response->headers->set('X-RateLimit-Limit', $maxAttempts);
        $response->headers->set('X-RateLimit-Remaining', $maxAttempts - RateLimiter::attempts($key));

        return $response;
    }
}

Output:

TEXT
// Execution Successful

6. Middleware Parameters

(1) Passing Parameters

PHP
// Route definition with parameters
Route::middleware('role:admin,super_admin')
    ->get('/admin/dashboard', [AdminController::class, 'dashboard']);

// Middleware receives parameters
class CheckRole
{
    public function handle(Request $request, Closure $next, string ...$roles): Response
    {
        if (!in_array($request->user()->role, $roles)) {
            abort(403, 'Insufficient role. Required: ' . implode(', ', $roles));
        }
        return $next($request);
    }
}

(2) The throttle parameter

PHP
// Built-in throttle with parameters
Route::middleware('throttle:60,1')->group(function () {
    // 60 requests per 1 minute
});

Route::middleware('throttle:10,1')->group(function () {
    // 10 requests per 1 minute (for expensive endpoints)
});

// Custom rate limiter
RateLimiter::for('api', function (Request $request) {
    return $request->user()
        ? Limit::perMinute(60)->by($request->user()->id)
        : Limit::perMinute(10)->by($request->ip());
});

RateLimiter::for('tenant-api', function (Request $request) {
    $tenant = app()->make(Tenant::class);
    $plan = $tenant->subscription?->plan;

    return Limit::perMinute(match ($plan->slug ?? 'starter') {
        'enterprise' => 300,
        'pro' => 120,
        default => 60,
    })->by($tenant->id);
});
Parameter Format Description Example
middleware:p1 Single-parameter role:admin
middleware:p1,p2 Multiple Parameters role:admin,editor
throttle:max,decay Throttling Parameters throttle:60,1

(1) ▶ Example: ShopMetrics Subscription-Based Rate Limiting

PHP
// app/Providers/AppServiceProvider.php
public function boot(): void
{
    RateLimiter::for('tenant-api', function (Request $request) {
        $tenant = app()->make(Tenant::class);
        $plan = $tenant->plan ?? null;

        return Limit::perMinute(match ($plan->slug ?? 'starter') {
            'enterprise' => 600,
            'pro' => 120,
            default => 60,
        })->by($tenant->id)->response(function () {
            return response()->json([
                'message' => 'Rate limit exceeded. Upgrade your plan for higher limits.',
                'upgrade_url' => route('pricing'),
            ], 429);
        });
    });
}

// routes/api.php
Route::middleware(['auth:sanctum', 'tenant', 'throttle:tenant-api'])
    ->prefix('v1')
    ->group(function () {
        Route::apiResource('shops', Api\ShopController::class);
    });

Output:

TEXT
// Execution Successful

7. Middleware Ordering and Priority

(1) Default Priority

Middleware is executed in the order it is registered, but certain middleware must be executed first (for example, CORS must be processed before authentication).

PHP
// bootstrap/app.php
->withMiddleware(function (Middleware $middleware) {
    $middleware->priority([
        \Illuminate\Foundation\Http\Middleware\HandlePrecognitiveRequests::class,
        \Illuminate\Http\Middleware\HandleCors::class,
        \App\Http\Middleware\PreventRequestsDuringMaintenance::class,
        \Illuminate\Http\Middleware\ValidatePostSize::class,
        \App\Http\Middleware\TenantResolve::class,  // Must run after CORS
        \App\Http\Middleware\Authenticate::class,
    ]);
})

(2) Terminable middleware

PHP
// app/Http/Middleware/SendAnalyticsEvent.php
class SendAnalyticsEvent
{
    public function handle(Request $request, Closure $next): Response
    {
        return $next($request);
    }

    // Runs AFTER response is sent to client
    public function terminate(Request $request, Response $response): void
    {
        // Non-blocking analytics logging
        AnalyticsEvent::create([
            'tenant_id' => app()->make(Tenant::class)?->id,
            'path' => $request->path(),
            'method' => $request->method(),
            'status' => $response->getStatusCode(),
            'duration_ms' => defined('LARAVEL_START')
                ? round((microtime(true) - LARAVEL_START) * 1000)
                : null,
        ]);
    }
}
Life Cycle Approach Timing Purpose
handle() (before) When a request is received Authenticate, filter, and modify the request
handle() (after) When the response is returned Modify the response headers
terminate() After sending a response Logs, Statistics, Cleanup

(1) ▶ Example: ShopMetrics Middleware Priority Configuration

PHP
// bootstrap/app.php
return Application::configure(basePath: dirname(__DIR__))
    ->withMiddleware(function (Middleware $middleware) {
        // Priority — CORS first, auth before tenant
        $middleware->priority([
            \Illuminate\Http\Middleware\HandleCors::class,
            \App\Http\Middleware\TenantResolve::class,
            \App\Http\Middleware\Authenticate::class,
            \App\Http\Middleware\CheckRole::class,
        ]);

        // Aliases
        $middleware->alias([
            'tenant' => \App\Http\Middleware\TenantResolve::class,
            'role' => \App\Http\Middleware\CheckRole::class,
            'tenant.throttle' => \App\Http\Middleware\PerTenantRateLimit::class,
        ]);

        // API group additions
        $middleware->appendToGroup('api', [
            \App\Http\Middleware\EnsureJsonAccept::class,
        ]);
    });

Output:

TEXT
// Execution Successful

8. Comprehensive Example: The ShopMetrics Middleware Architecture

PHP
// ============================================
// Comprehensive: ShopMetrics Middleware Stack
// Covers: global, route, groups, params, priority, terminable
// ============================================

// app/Http/Middleware/TenantResolve.php
class TenantResolve
{
    public function handle(Request $request, Closure $next): Response
    {
        $user = $request->user();
        if (!$user?->tenant_id) {
            abort(403, 'No tenant associated.');
        }
        $tenant = $user->tenant;
        if ($tenant->status !== 'active') {
            abort(403, 'Tenant is ' . $tenant->status);
        }
        app()->instance(Tenant::class, $tenant);
        return $next($request);
    }
}

// app/Http/Middleware/CheckRole.php
class CheckRole
{
    public function handle(Request $request, Closure $next, string ...$roles): Response
    {
        if (!in_array($request->user()?->role, $roles)) {
            abort(403, 'Required role: ' . implode('|', $roles));
        }
        return $next($request);
    }
}

// app/Http/Middleware/EnsureSubscriptionActive.php
class EnsureSubscriptionActive
{
    public function handle(Request $request, Closure $next): Response
    {
        $tenant = app()->make(Tenant::class);
        $subscription = $tenant->subscription;
        if (!$subscription || $subscription->status !== 'active') {
            abort(402, 'Active subscription required.');
        }
        return $next($request);
    }
}

// app/Http/Middleware/LogApiRequests.php
class LogApiRequests
{
    public function handle(Request $request, Closure $next): Response
    {
        return $next($request);
    }

    public function terminate(Request $request, Response $response): void
    {
        ApiLog::create([
            'tenant_id' => app()->make(Tenant::class)?->id,
            'user_id' => $request->user()?->id,
            'method' => $request->method(),
            'path' => $request->path(),
            'status' => $response->getStatusCode(),
            'ip' => $request->ip(),
        ]);
    }
}

// Routes using the full middleware stack
Route::middleware(['auth:sanctum', 'tenant', 'throttle:tenant-api', 'subscription.active'])
    ->prefix('v1')->group(function () {
        Route::apiResource('shops', ShopController::class);
        Route::middleware('role:tenant_owner,analyst')
            ->apiResource('analytics', AnalyticsController::class)->only(['index', 'show']);
        Route::middleware('role:tenant_owner')
            ->apiResource('settings', SettingsController::class);
    });

❓ FAQ

Q What is the difference between route middleware and controller constructor middleware?
A Route middleware is applied when a route is matched, making it more flexible (it can be customized per route); controller constructor middleware is executed when the controller is instantiated, making it suitable for all methods within the controller. Route middleware should be used whenever possible.
Q When is the terminate method executed?
A It is executed after the response is sent to the client (similar to register_shutdown_function). It is suitable for operations such as logging and statistics that do not need to affect the response. terminate does not block the user.
Q What problems can arise if the order of the middleware is incorrect?
A The CORS middleware must be executed before authentication; otherwise, the preflight request (OPTIONS) will be intercepted by the authentication middleware and return a 401 error. TenantResolve must be executed before the business middleware; otherwise, tenant() will be null.
Q How do I short-circuit a request in a middleware?
A Return a Response object directly without calling $next($request): return response()->json(['error' => 'Unauthorized'], 401);. Subsequent middleware and controllers will not be executed.
Q Can global middleware exclude certain routes?
A In Laravel 11, you can use $middleware->skipWhen(callback) or $request->is('api/*') within the middleware to determine whether to skip a route. It is not recommended to include route checks in global middleware.
Q What is the relationship between the RateLimiter and the throttle middleware?
A The throttle middleware uses the RateLimiter Facade internally. When creating custom rate-limiting logic, first define the strategy in RateLimiter::for(), then reference it in the route using throttle:策略名.

📖 Summary


📝 Exercises

  1. Basic Exercise (⭐): Create a TenantResolve middleware that resolves the tenant from an authenticated user and stores it in a container, then use it in an API route group. Test that users without a bound tenant receive a 403 error when they attempt to access the API.

  2. Advanced Exercise (⭐⭐): Implement the PerTenantRateLimit middleware to enforce rate limiting based on tenant ID and IP address, dynamically setting rate limits according to the subscription plan (Starter: 60/min, Pro: 120/min, Enterprise: 600/min).

  3. Challenge (⭐⭐⭐): Design a complete middleware priority system—CORS → TenantResolve → Auth → SubscriptionActive → CheckRole → throttle—to ensure each middleware executes at the correct time. Write tests to verify that OPTIONS pre-check requests are not intercepted by authentication.

Web-Tutorial.com

Web-Tutorial Tech Team

A team of developers maintaining programming tutorials. Each tutorial is written and reviewed by developers with expertise in that field. We work to keep our content accurate and reliable — if you spot an issue, please let us know.

100%

🙏 帮我们做得更好

我们是刚上线的编程教程站,几个人的小团队,精力有限。页面虽经检查,难免还有疏漏——链接失效、排版错乱、内容有误、语言生硬……

如果您发现了,麻烦告诉我们,我们会在收到反馈后第一时间进行修复,再次感谢您的光临 🙏