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
- Middleware Execution Flow and the Pipeline Pattern
- Three registration methods: global, route, and middleware group
- Custom middleware: Rate Limiting / CORS / Tenant Resolution
- Middleware parameter passing: throttle:60,1 / role:admin
- Middleware Sorting and Priority Control
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.
// 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
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
Request →
Middleware A (before) →
Middleware B (before) →
Controller → Response
Middleware B (after) →
Middleware A (after) →
Response
Each middleware can:
- Before: Execute logic before the request reaches the controller
- After: Execute logic after the controller returns a response
- Short-circuit: Returns a response immediately without passing it to the next layer
(1) ▶ Example: Understanding the Onion Model of Middleware
// 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:
// Execution Successful
4. Three Ways to Register
(1) Global Middleware
Every request is executed; there is no need to specify this in the route.
// 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.
// 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 |
// 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
// 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:
// Execution Successful
5. Custom Middleware
(1) Create middleware
php artisan make:middleware TenantResolve
php artisan make:middleware CheckRole
php artisan make:middleware EnsureJsonAccept
(2) TenantResolve Middleware
// 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
// 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
// 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:
// Execution Successful
6. Middleware Parameters
(1) Passing Parameters
// 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
// 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
// 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:
// 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).
// 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
// 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
// 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:
// Execution Successful
8. Comprehensive Example: The ShopMetrics Middleware Architecture
// ============================================
// 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
terminate method executed?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.return response()->json(['error' => 'Unauthorized'], 401);. Subsequent middleware and controllers will not be executed.$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.throttle:策略名.📖 Summary
- Middleware runs according to the onion model: before → next → after
- Three registration methods: global, route alias, and middleware group
- Custom middleware is created using
make:middlewareand used after registering an alias. - Middleware parameters are separated by colons:
role:admin,editor - Priority determines the order of execution; CORS must take precedence over authentication
terminate()is executed after the response is sent; it is suitable for logging and statistics.
📝 Exercises
-
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.
-
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).
-
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.



