深入理解 Laravel Middleware:完整指南

Laravel 中间件是框架最强大的特性之一,它在 HTTP 请求和应用核心逻辑之间扮演着桥梁的角色。不管你是开发简单的博客还是复杂的企业应用,掌握中间件都是写出安全、易维护、高效代码的关键。

这篇指南会带你全面了解 Laravel 12 中间件,从基础概念到高级用法和最佳实践。

什么是中间件?

中间件提供了一种便捷的机制来检查和过滤进入应用的 HTTP 请求。你可以把中间件理解为 HTTP 请求在到达应用核心之前必须经过的一道道关卡。

比如,Laravel 内置了一个用于验证用户身份的中间件。如果用户未登录,中间件会把他们重定向到登录页。如果已登录,中间件就放行,让请求继续往下走。

除了身份验证,中间件还有很多其他用途:

  • 请求日志:跟踪所有传入请求以进行调试和分析
  • CSRF 保护:确保请求合法且安全
  • 数据验证:在数据到达控制器之前进行验证
  • 速率限制:通过限制请求频率来防止滥用
  • CORS 处理:管理跨域资源共享策略
  • API Token 验证:认证 API 请求
  • 基于角色的访问控制:根据用户角色限制访问
  • 请求/响应修改:在处理或发送之前转换数据

中间件的工作原理:请求生命周期

理解中间件在 Laravel 请求生命周期中的位置很重要。当一个 HTTP 请求进来时,它会经历这样的流程:

  1. 请求从 public/index.php 进入
  2. Laravel 启动应用并加载服务提供者
  3. 请求经过全局中间件栈
  4. 路由器匹配对应的路由
  5. 执行该路由的中间件
  6. 请求到达控制器或路由处理器
  7. 响应原路返回,再次经过中间件
  8. 最终返回给客户端

这种管道式架构让每个中间件都可以在请求到达应用逻辑之前对其进行检查、修改,甚至直接拦截。

创建自定义中间件

在 Laravel 12 中,用 Artisan 命令创建中间件很简单。我们来一步步看如何创建自定义中间件。

步骤 1:生成 Middleware

使用 make:middleware Artisan 命令创建一个新的 Middleware 类:

bash
php artisan make:middleware EnsureTokenIsValid

这个命令会在 app/Http/Middleware 目录下生成一个新文件,里面已经写好了基本的中间件结构。

步骤 2:编写中间件逻辑

打开刚生成的 EnsureTokenIsValid.php,你会看到一个带 handle 方法的模板:

php
<?php

namespace App\Http\Middleware;

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

class EnsureTokenIsValid
{
    /**
     * Handle an incoming request.
     *
     * @param  \Closure(\Illuminate\Http\Request): (\Symfony\Component\HttpFoundation\Response)  $next
     */
    public function handle(Request $request, Closure $next): Response
    {
        if ($request->input('token') !== 'my-secret-token') {
            return redirect('/home')->with('error', 'Invalid token provided');
        }

        return $next($request);
    }
}

handle 方法有两个参数:

  • $request:当前的 HTTP 请求
  • $next:下一个中间件的闭包

如果想让请求通过,就调用 $next($request)。如果要拦截请求,就直接返回响应或重定向。

步骤 3:前置和后置中间件

中间件可以在处理请求前后都执行操作。

前置中间件(在请求到达控制器之前执行):

php
<?php

namespace App\Http\Middleware;

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

class BeforeMiddleware
{
    public function handle(Request $request, Closure $next): Response
    {
        // 在请求被处理之前执行操作
        Log::info('Request received: ' . $request->path());
        
        return $next($request);
    }
}

后置中间件(在请求处理完成后执行):

php
<?php

namespace App\Http\Middleware;

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

class AfterMiddleware
{
    public function handle(Request $request, Closure $next): Response
    {
        $response = $next($request);
        
        // 在请求被处理之后执行操作
        Log::info('Response sent: ' . $response->getStatusCode());
        
        return $response;
    }
}

注册中间件

Laravel 12 有个重大变化:中间件注册不再用 app/Http/Kernel.php,而是移到了 bootstrap/app.php。这样做集中了配置,也更好理解。

Bootstrap/App.php 的新结构

Laravel 12 中 bootstrap/app.php 的基本结构长这样:

php
<?php

use Illuminate\Foundation\Application;

return Application::configure(basePath: dirname(__DIR__))
    ->withRouting(
        web: __DIR__.'/../routes/web.php',
        api: __DIR__.'/../routes/api.php',
        commands: __DIR__.'/../routes/console.php',
        health: '/up',
    )
    ->withMiddleware(function ($middleware) {
        // 在这里注册 Middleware
    })
    ->withExceptions(function ($exceptions) {
        //
    })
    ->create();

全局中间件

全局中间件会在每个 HTTP 请求上运行。注册方式是在 withMiddleware 闭包中用 appendprepend

php
->withMiddleware(function ($middleware) {
    $middleware->append(\App\Http\Middleware\EnsureTokenIsValid::class);
})

append 会把中间件加到栈的末尾,prepend 则加到开头:

php
->withMiddleware(function ($middleware) {
    $middleware->prepend(\App\Http\Middleware\LogRequests::class);
    $middleware->append(\App\Http\Middleware\CompressResponse::class);
})

路由中间件

路由中间件只作用于指定的路由或路由组。注册时需要给中间件起个别名,用 alias 方法:

php
->withMiddleware(function ($middleware) {
    $middleware->alias([
        'admin' => \App\Http\Middleware\CheckAdmin::class,
        'verified.email' => \App\Http\Middleware\EnsureEmailIsVerified::class,
        'check.token' => \App\Http\Middleware\EnsureTokenIsValid::class,
    ]);
})

中间件组

中间件组可以把多个中间件打包在一起,方便批量应用:

php
->withMiddleware(function ($middleware) {
    $middleware->appendToGroup('api', [
        \App\Http\Middleware\EncryptCookies::class,
        \Illuminate\Cookie\Middleware\AddQueuedCookiesToResponse::class,
    ]);
})

Laravel 12 已经内置了 webapi 组。你可以向它们添加自己的中间件,也可以创建新的组。

应用中间件到路由

注册好中间件后,有多种方式可以应用到路由上。

单个路由

middleware 方法给单个路由加中间件:

php
use Illuminate\Support\Facades\Route;

Route::get('/dashboard', function () {
    // Dashboard 逻辑
})->middleware('auth');

多个中间件

传一个数组就可以同时应用多个:

php
Route::get('/admin/settings', function () {
    // 管理员设置
})->middleware(['auth', 'admin', 'verified.email']);

路由组

给一组路由加中间件:

php
Route::middleware(['auth', 'verified.email'])->group(function () {
    Route::get('/profile', [ProfileController::class, 'show']);
    Route::put('/profile', [ProfileController::class, 'update']);
    Route::delete('/profile', [ProfileController::class, 'destroy']);
});

在控制器中使用

也可以直接在控制器构造函数中指定:

php
<?php

namespace App\Http\Controllers;

class AdminController extends Controller
{
    public function __construct()
    {
        $this->middleware('auth');
        $this->middleware('admin')->only(['destroy', 'create']);
        $this->middleware('log.request')->except(['index']);
    }
}

中间件参数

中间件可以接收参数,这样能让一个中间件更灵活、更好复用。比如做权限控制或功能开关时就特别有用。

创建带参数的中间件

下面是一个检查用户角色的例子:

php
<?php

namespace App\Http\Middleware;

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

class CheckRole
{
    public function handle(Request $request, Closure $next, string $role): Response
    {
        if (! $request->user() || ! $request->user()->hasRole($role)) {
            abort(403, 'Unauthorized action.');
        }

        return $next($request);
    }
}

使用带参数的中间件

用冒号把参数传给中间件:

php
Route::get('/admin/dashboard', function () {
    // 仅限管理员
})->middleware('role:admin');

Route::get('/moderator/panel', function () {
    // 仅限版主
})->middleware('role:moderator');

也可以传多个参数,用逗号隔开:

php
Route::get('/content/edit', function () {
    // 编辑内容
})->middleware('role:admin,editor');

中间件会把它们当作独立的参数接收:

php
public function handle(Request $request, Closure $next, string ...$roles): Response
{
    if (! $request->user() || ! $request->user()->hasAnyRole($roles)) {
        abort(403, 'Unauthorized action.');
    }

    return $next($request);
}

依赖注入

Laravel 的服务容器会解析所有中间件,所以你可以在构造函数中直接注入需要的依赖:

php
<?php

namespace App\Http\Middleware;

use App\Services\TokenService;
use Closure;
use Illuminate\Http\Request;
use Symfony\Component\HttpFoundation\Response;

class ValidateApiToken
{
    protected $tokenService;

    public function __construct(TokenService $tokenService)
    {
        $this->tokenService = $tokenService;
    }

    public function handle(Request $request, Closure $next): Response
    {
        $token = $request->header('X-API-Token');
        
        if (! $this->tokenService->isValid($token)) {
            return response()->json(['error' => 'Invalid API token'], 401);
        }

        return $next($request);
    }
}

可终止中间件

有时候你想在响应发送给用户之后再做一些事情。这时可以实现 terminate 方法:

php
<?php

namespace App\Http\Middleware;

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

class TerminableMiddleware
{
    public function handle(Request $request, Closure $next): Response
    {
        return $next($request);
    }

    public function terminate(Request $request, Response $response): void
    {
        // 在响应发送后执行任务
        // 这不会延迟对用户的响应
        Log::info('Response completed', [
            'status' => $response->getStatusCode(),
            'path' => $request->path(),
        ]);
    }
}

terminate 方法会在响应发出后才执行,所以不会影响用户体验。适合用来记日志、同步数据或做一些清理工作。

实际案例

看几个在生产环境中常用的中间件实现。

API 限流中间件

php
<?php

namespace App\Http\Middleware;

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

class RateLimitApi
{
    public function handle(Request $request, Closure $next, int $maxAttempts = 60): Response
    {
        $key = 'rate-limit:' . $request->ip();
        $attempts = Cache::get($key, 0);

        if ($attempts >= $maxAttempts) {
            return response()->json([
                'error' => 'Too many requests. Please try again later.'
            ], 429);
        }

        Cache::put($key, $attempts + 1, now()->addMinute());

        $response = $next($request);
        
        $response->headers->set('X-RateLimit-Limit', $maxAttempts);
        $response->headers->set('X-RateLimit-Remaining', $maxAttempts - $attempts - 1);

        return $response;
    }
}

请求日志中间件

php
<?php

namespace App\Http\Middleware;

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

class LogRequests
{
    public function handle(Request $request, Closure $next): Response
    {
        $startTime = microtime(true);
        
        $response = $next($request);
        
        $duration = microtime(true) - $startTime;
        
        Log::info('HTTP Request', [
            'method' => $request->method(),
            'url' => $request->fullUrl(),
            'ip' => $request->ip(),
            'user_id' => $request->user()?->id,
            'status' => $response->getStatusCode(),
            'duration' => round($duration * 1000, 2) . 'ms',
        ]);

        return $response;
    }
}

强制 HTTPS 中间件

php
<?php

namespace App\Http\Middleware;

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

class ForceHttps
{
    public function handle(Request $request, Closure $next): Response
    {
        if (! $request->secure() && app()->environment('production')) {
            return redirect()->secure($request->getRequestUri(), 301);
        }

        return $next($request);
    }
}

输入清理中间件

php
<?php

namespace App\Http\Middleware;

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

class SanitizeInput
{
    public function handle(Request $request, Closure $next): Response
    {
        $input = $request->all();
        
        array_walk_recursive($input, function (&$value) {
            if (is_string($value)) {
                $value = strip_tags($value);
                $value = trim($value);
            }
        });
        
        $request->merge($input);

        return $next($request);
    }
}

语言切换中间件

php
<?php

namespace App\Http\Middleware;

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

class SetLocale
{
    public function handle(Request $request, Closure $next): Response
    {
        $locale = $request->segment(1);
        $availableLocales = ['en', 'es', 'fr', 'de'];
        
        if (in_array($locale, $availableLocales)) {
            App::setLocale($locale);
        }

        return $next($request);
    }
}

Laravel 内置的中间件

Laravel 12 内置了一些强大的中间件来处理常见任务:

身份验证

  • Authenticate:确保用户已登录
  • RedirectIfAuthenticated:已登录用户访问登录页时自动跳转

CSRF 保护

  • VerifyCsrfToken:防止 POST、PUT、PATCH 和 DELETE 请求的跨站请求伪造攻击

Session 管理

  • StartSession:处理 Session 数据
  • ShareErrorsFromSession:把验证错误传递给视图
  • EncryptCookies:自动加密解密 Cookie
  • AddQueuedCookiesToResponse:把队列中的 Cookie 加入响应

安全相关

  • HandleCors:处理跨域请求 (CORS)
  • TrustProxies:配置可信代理,用于负载均衡场景

维护模式

  • PreventRequestsDuringMaintenance:维护模式下阻止请求

最佳实践

跟着这些建议做,可以让你的中间件更好维护、更安全、性能也更好:

保持简单专注

一个中间件只做一件事。如果一个中间件做的事情太多,就该拆分了:

php
// 坏例子:一个中间件做太多事
class HandleRequestMiddleware
{
    public function handle($request, $next)
    {
        // 认证
        // 验证
        // 日志
        // 转换数据
        // 等等...
    }
}

// 好例子:拆分成多个中间件
class AuthenticateMiddleware { }
class ValidateRequestMiddleware { }
class LogRequestMiddleware { }
class TransformRequestMiddleware { }

用参数提高灵活性

让中间件接受参数,能提高复用性:

php
// 不要创建 CheckAdminMiddleware、CheckModeratorMiddleware 等
// 写一个灵活的就够了
class CheckRole
{
    public function handle($request, $next, ...$roles)
    {
        if (! $request->user()->hasAnyRole($roles)) {
            abort(403);
        }
        return $next($request);
    }
}

// 使用方式
Route::get('/admin', fn() => '...')->middleware('role:admin');
Route::get('/content', fn() => '...')->middleware('role:admin,editor');

注意执行顺序

中间件的顺序很重要。认证中间件应该放在需要用户信息的中间件之前:

php
Route::middleware(['auth', 'verified', 'role:admin'])->group(function () {
    // 这里的路由
});

不要滥用

不是所有逻辑都适合放在中间件里。中间件适合处理多个路由的通用逻辑。如果是特定路由的逻辑,考虑用:

  • 控制器方法
  • 表单请求验证类
  • 服务类
  • 路由模型绑定

优化性能

中间件逻辑要尽量轻量。如果有耗时操作:

  • 用缓存避免重复计算
  • 把耗时任务放到队列
  • 用可终止中间件做响应后处理
php
public function handle($request, $next)
{
    // 只做快速检查
    if (Cache::has('user-banned:' . $request->user()->id)) {
        abort(403);
    }
    
    return $next($request);
}

public function terminate($request, $response)
{
    // 耗时操作放到响应后
    SomeHeavyJob::dispatch($request->user());
}

处理好异常

中间件里要做好异常处理,避免应用崩溃:

php
public function handle($request, $next)
{
    try {
        // 验证 Token
        $token = $request->header('X-API-Token');
        $this->tokenService->validate($token);
    } catch (InvalidTokenException $e) {
        return response()->json(['error' => 'Invalid token'], 401);
    }
    
    return $next($request);
}

写测试

给中间件写测试,确保它们正常工作:

php
<?php

namespace Tests\Feature;

use Tests\TestCase;

class CheckAdminMiddlewareTest extends TestCase
{
    public function test_non_admin_cannot_access_admin_routes()
    {
        $user = User::factory()->create(['role' => 'user']);
        
        $response = $this->actingAs($user)->get('/admin/dashboard');
        
        $response->assertStatus(403);
    }
    
    public function test_admin_can_access_admin_routes()
    {
        $admin = User::factory()->create(['role' => 'admin']);
        
        $response = $this->actingAs($admin)->get('/admin/dashboard');
        
        $response->assertStatus(200);
    }
}

加上类型声明

用上 PHP 的类型系统,IDE 也能更好地提示:

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

public function handle(Request $request, Closure $next): Response
{
    // 实现
}

注释要清楚

如果中间件有参数,记得加注释说明:

php
/**
 * 检查用户是否具有所需角色。
 *
 * @param  \Illuminate\Http\Request  $request
 * @param  \Closure  $next
 * @param  string  ...$roles  所需角色(admin、editor、moderator)
 * @return \Symfony\Component\HttpFoundation\Response
 */
public function handle(Request $request, Closure $next, string ...$roles): Response
{
    // 实现
}

安全相关

中间件在应用安全中很关键。注意这几点:

别忘了 CSRF 保护

Laravel 的 VerifyCsrfToken 中间件要加入 web 组,用来保护所有修改数据的请求:

php
->withMiddleware(function ($middleware) {
    $middleware->appendToGroup('web', [
        \Illuminate\Foundation\Http\Middleware\VerifyCsrfToken::class,
    ]);
})

API Token 验证

API 路由要做好 Token 验证:

php
public function handle($request, $next)
{
    $token = $request->bearerToken();
    
    if (! $token || ! $this->isValidToken($token)) {
        return response()->json(['error' => 'Unauthorized'], 401);
    }
    
    return $next($request);
}

限流保护

加上限流,防止被恶意攻击,特别是 API 和登录接口:

php
Route::middleware(['throttle:60,1'])->group(function () {
    // 每分钟最多 60 次请求
});

输入清理

虽然 Laravel 已经有 CSRF 和 SQL 注入防护,但在中间件里再加一层清理也不错:

php
public function handle($request, $next)
{
    $input = $request->all();
    
    array_walk_recursive($input, function (&$value) {
        if (is_string($value)) {
            $value = htmlspecialchars($value, ENT_QUOTES, 'UTF-8');
        }
    });
    
    $request->merge($input);
    
    return $next($request);
}

敏感路由要保护好

给敏感路由加多层中间件保护:

php
Route::middleware(['auth', 'verified', '2fa', 'role:admin'])->group(function () {
    // 敏感的管理后台路由
});

调试技巧

如果中间件不正常,试试这些方法:

启用查询日志

php
public function handle($request, $next)
{
    DB::enableQueryLog();
    
    $response = $next($request);
    
    Log::debug('Queries executed:', DB::getQueryLog());
    
    return $response;
}

记录执行日志

php
public function handle($request, $next)
{
    Log::debug('Middleware executed: ' . static::class);
    
    return $next($request);
}

用 Telescope 调试

Laravel Telescope 能帮你监控中间件执行、请求处理和性能指标。开发环境安装一下:

bash
composer require laravel/telescope --dev
php artisan telescope:install
php artisan migrate

中间件 vs. 其他功能

什么时候用中间件,什么时候用其他特性?看这里:

vs. Form Requests

  • 用中间件:多个路由的通用逻辑
  • 用 Form Request:单个路由的验证逻辑

vs. Gates 和 Policies

  • 用中间件:路由级别的权限检查
  • 用 Gates/Policies:控制器里的资源权限检查

vs. Service 类

  • 用中间件:HTTP 请求/响应相关的操作
  • 用 Service 类:跟 HTTP 无关的业务逻辑

vs. Events 和 Listeners

  • 用中间件:同步处理请求/响应
  • 用 Events/Listeners:解耦的、可能异步的操作

性能优化

这些方法能提升中间件性能:

缓存耗时操作

php
public function handle($request, $next)
{
    $userId = $request->user()->id;
    $permissions = Cache::remember(
        "user-permissions:{$userId}",
        3600,
        fn() => $this->permissionService->getUserPermissions($userId)
    );
    
    $request->merge(['permissions' => $permissions]);
    
    return $next($request);
}

尽早返回

不符合条件就赶紧返回,别继续执行:

php
public function handle($request, $next)
{
    if (! $request->user()) {
        return redirect('/login');
    }
    
    if (! $request->user()->isActive()) {
        return response('Account suspended', 403);
    }
    
    return $next($request);
}

按需加载

不要一开始就加载所有依赖,用到再加载:

php
public function handle($request, $next)
{
    // 仅在需要时加载服务
    if ($request->has('validate')) {
        app(ValidationService::class)->validate($request);
    }
    
    return $next($request);
}

常见错误

写中间件时避免这些坑:

忘记 return

必须 return $next($request) 的结果:

php
// 错误
public function handle($request, $next)
{
    if (! $request->user()) {
        redirect('/login'); // 缺少 return!
    }
    $next($request); // 缺少 return!
}

// 正确
public function handle($request, $next)
{
    if (! $request->user()) {
        return redirect('/login');
    }
    return $next($request);
}

在 $next 之后改请求

调用 $next() 后再改请求已经没用了:

php
// 错误
public function handle($request, $next)
{
    $response = $next($request);
    $request->merge(['foo' => 'bar']); // 已经晚了
    return $response;
}

// 正确
public function handle($request, $next)
{
    $request->merge(['foo' => 'bar']);
    return $next($request);
}

忘记注册

用之前记得在 bootstrap/app.php 里注册。

顺序错了

注意顺序,认证要放在权限检查之前:

php
// 错误的顺序
Route::middleware(['role:admin', 'auth'])

// 正确的顺序
Route::middleware(['auth', 'role:admin'])

从 Laravel 11 升级

如果你是从 Laravel 11 升级来的,中间件注册方式改了:

老写法(Laravel 11)

php
protected $middleware = [
    \App\Http\Middleware\TrustProxies::class,
];

protected $middlewareGroups = [
    'web' => [
        \App\Http\Middleware\EncryptCookies::class,
    ],
];

protected $routeMiddleware = [
    'auth' => \App\Http\Middleware\Authenticate::class,
];

新写法(Laravel 12)

php
->withMiddleware(function ($middleware) {
    // 全局 Middleware
    $middleware->append(\App\Http\Middleware\TrustProxies::class);
    
    // Middleware 组
    $middleware->appendToGroup('web', [
        \App\Http\Middleware\EncryptCookies::class,
    ]);
    
    // 路由 Middleware 别名
    $middleware->alias([
        'auth' => \App\Http\Middleware\Authenticate::class,
    ]);
})

总结

Laravel 中间件是个强大的特性,能以干净、可复用的方式处理通用逻辑。掌握好中间件的用法和最佳实践,就能写出更安全、更好维护、性能更好的 Laravel 应用。

重点回顾:

  • 中间件是 HTTP 请求的过滤器
  • Laravel 12 把注册从 Kernel.php 移到了 bootstrap/app.php
  • 有全局、路由和组三种中间件
  • 中间件可以接收参数,提高灵活性
  • 保持简单专注,记得写测试
  • 适用于安全、日志、数据转换等通用场景
  • 注重性能、安全和可维护性

随着使用的深入,你会发现更多中间件的用法,也会形成自己的实现模式。记住一点:中间件要保持专注、逻辑清晰、文档齐全。

本作品采用《CC 协议》,转载必须注明作者和本文链接