Laravel: Laravel中间件深度解析
最后更新:2026-08-26
中间件是 Laravel 的"安检流水线"——每个请求像行李一样穿过层层检查,不合格的直接拦截,合格的放行到下一站。
1. 你将学到
- 中间件执行流程与 Pipeline 模式
- 全局/路由/中间件组三种注册方式
- 自定义中间件:Rate Limiting / CORS / Tenant Resolution
- 中间件参数传递:throttle:60,1 / role:admin
- 中间件排序与优先级控制
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) 请求穿越中间件的全过程
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
每个中间件都可以:
- Before:在请求到达控制器前执行逻辑
- After:在控制器返回响应后执行逻辑
- Short-circuit:直接返回响应,不传递到下一层
▶ 示例:理解中间件洋葱模型
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:策略名 引用。📖 小节
- 中间件按洋葱模型执行:before → next → after
- 三种注册方式:全局、路由别名、中间件组
- 自定义中间件用 make:middleware 创建,注册别名后使用
- 中间件参数用冒号分隔:
role:admin,editor - Priority 控制执行顺序,CORS 必须优先于认证
- terminate() 在响应发送后执行,适合日志统计
📝 作业
-
基础题(⭐):创建 TenantResolve 中间件,从认证用户解析租户并存入容器,在 API 路由组中使用,测试未绑定租户的用户访问返回 403。
-
进阶题(⭐⭐):实现 PerTenantRateLimit 中间件,按租户 ID 和 IP 限流,根据订阅计划动态设置限流阈值(Starter 60/min、Pro 120/min、Enterprise 600/min)。
-
挑战题(⭐⭐⭐):设计完整的中间件优先级体系——CORS → TenantResolve → Auth → SubscriptionActive → CheckRole → throttle,确保每个中间件在正确的时机执行,编写测试验证 OPTIONS 预检请求不被认证拦截。