Laravel: Laravel中间件深度解析

最后更新:2026-08-26

中间件是 Laravel 的"安检流水线"——每个请求像行李一样穿过层层检查,不合格的直接拦截,合格的放行到下一站。

1. 你将学到


2. 一个架构师的真实故事

(1) 痛点:所有安全检查都堆在控制器里

Bob 在每个控制器方法开头都写了 10 行检查代码——认证检查、租户隔离、限流、CORS、日志记录。50 个方法 × 10 行 = 500 行重复代码。Alice 加了新的安全策略,要改 50 个地方,漏了 3 个导致漏洞。Charlie 说:"你们听过中间件吗?"

(2) 中间件的解法

中间件把通用检查逻辑提取到独立类中,每个请求自动穿过多层中间件——认证、限流、CORS、租户隔离——控制器只写业务逻辑。

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) 收益

Bob 用中间件后,控制器代码减少 60%,Alice 的新安全策略只需改 1 个中间件,0 遗漏。


3. 中间件 Pipeline 执行流

(1) 请求穿越中间件的全过程

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) 洋葱模型

TEXT 📖 仅展示
Request →
  Middleware A (before) →
    Middleware B (before) →
      Controller → Response
    Middleware B (after) →
  Middleware A (after) →
Response

每个中间件都可以:

▶ 示例:理解中间件洋葱模型

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;
    }
}

输出:

TEXT 📖 仅展示
// 执行成功

4. 三种注册方式

(1) 全局中间件

每个请求都执行,无需在路由中指定。

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) 路由中间件

在路由定义中指定的中间件。

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) 中间件组

一组中间件打包,按组应用。

组名 默认包含 适用
web StartSession, EncryptCookies, VerifyCsrfToken Web 路由
api Throttle:api, SubstitueBindings API 路由
PHP
// Customize middleware groups
->withMiddleware(function (Middleware $middleware) {
    $middleware->appendToGroup('api', [
        \App\Http\Middleware\TenantResolve::class,
    ]);

    $middleware->prependToGroup('web', [
        \App\Http\Middleware\SetLocale::class,
    ]);
})
注册方式 执行时机 适用场景
全局 所有请求 日志、CORS
路由别名 指定路由 认证、租户
中间件组 组内路由 web/api 分组

▶ 示例:ShopMetrics 中间件注册

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,
    ]);
})

输出:

TEXT 📖 仅展示
// 执行成功

5. 自定义中间件

(1) 创建中间件

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

(2) TenantResolve 中间件

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 中间件

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;
    }
}

▶ 示例:ShopMetrics 限流中间件

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;
    }
}

输出:

TEXT 📖 仅展示
// 执行成功

6. 中间件参数

(1) 传递参数

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) throttle 参数

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);
});
参数格式 说明 示例
middleware:p1 单参数 role:admin
middleware:p1,p2 多参数 role:admin,editor
throttle:max,decay 限流参数 throttle:60,1

▶ 示例:ShopMetrics 基于订阅的限流

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);
    });

输出:

TEXT 📖 仅展示
// 执行成功

7. 中间件排序与优先级

(1) 默认优先级

中间件按注册顺序执行,但某些中间件需要优先执行(如 CORS 必须在认证前)。

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) 可终止中间件

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,
        ]);
    }
}
生命周期方法 时机 用途
handle() (before) 请求进入时 认证、过滤、修改请求
handle() (after) 响应返回时 修改响应头
terminate() 响应发送后 日志、统计、清理

▶ 示例:ShopMetrics 中间件优先级配置

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,
        ]);
    });

输出:

TEXT 📖 仅展示
// 执行成功

8. 综合示例:ShopMetrics 中间件体系

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);
    });

❓ 常见问题

Q 中间件和控制器构造函数 middleware 有什么区别?
A 路由中间件在路由匹配时就确定,更灵活(可按路由定制);构造函数 middleware 在控制器实例化时执行,适合控制器内部所有方法。优先用路由中间件。
Q terminate 方法什么时候执行?
A 在响应发送给客户端之后执行(类似于 register_shutdown_function),适合日志、统计等不需要影响响应的操作。terminate 不会阻塞用户。
Q 中间件顺序错了会导致什么问题?
A CORS 中间件必须在认证前执行,否则预检请求(OPTIONS)会被认证中间件拦截返回 401;TenantResolve 必须在业务中间件前执行,否则 tenant() 为 null。
Q 如何在中间件中短路请求?
A 直接返回 Response 对象而不调用 $next($request):return response()->json(['error' => 'Unauthorized'], 401);。后续中间件和控制器都不会执行。
Q 全局中间件能排除某些路由吗?
A Laravel 11 中可以用 $middleware->skipWhen(callback) 或在中间件内部用 $request->is('api/*') 判断跳过。不推荐在全局中间件中写路由判断。
Q RateLimiter 和 throttle 中间件有什么关系?
A throttle 中间件内部使用 RateLimiter Facade。自定义限流逻辑时先在 RateLimiter::for() 中定义策略,然后在路由中用 throttle:策略名 引用。

📖 小节


📝 作业

  1. 基础题(⭐):创建 TenantResolve 中间件,从认证用户解析租户并存入容器,在 API 路由组中使用,测试未绑定租户的用户访问返回 403。

  2. 进阶题(⭐⭐):实现 PerTenantRateLimit 中间件,按租户 ID 和 IP 限流,根据订阅计划动态设置限流阈值(Starter 60/min、Pro 120/min、Enterprise 600/min)。

  3. 挑战题(⭐⭐⭐):设计完整的中间件优先级体系——CORS → TenantResolve → Auth → SubscriptionActive → CheckRole → throttle,确保每个中间件在正确的时机执行,编写测试验证 OPTIONS 预检请求不被认证拦截。

Web-Tutorial.com

Web-Tutorial 技术团队

由多位开发者共同维护的编程教程平台。每篇教程由对应领域的开发者编写和审核,确保内容准确可靠。如发现任何问题,欢迎向我们反馈。

100%

🙏 帮我们做得更好

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

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