Laravel 中间件是框架最强大的特性之一,它在 HTTP 请求和应用核心逻辑之间扮演着桥梁的角色。不管你是开发简单的博客还是复杂的企业应用,掌握中间件都是写出安全、易维护、高效代码的关键。
这篇指南会带你全面了解 Laravel 12 中间件,从基础概念到高级用法和最佳实践。
中间件提供了一种便捷的机制来检查和过滤进入应用的 HTTP 请求。你可以把中间件理解为 HTTP 请求在到达应用核心之前必须经过的一道道关卡。
比如,Laravel 内置了一个用于验证用户身份的中间件。如果用户未登录,中间件会把他们重定向到登录页。如果已登录,中间件就放行,让请求继续往下走。
除了身份验证,中间件还有很多其他用途:
理解中间件在 Laravel 请求生命周期中的位置很重要。当一个 HTTP 请求进来时,它会经历这样的流程:
public/index.php 进入这种管道式架构让每个中间件都可以在请求到达应用逻辑之前对其进行检查、修改,甚至直接拦截。
在 Laravel 12 中,用 Artisan 命令创建中间件很简单。我们来一步步看如何创建自定义中间件。
使用 make:middleware Artisan 命令创建一个新的 Middleware 类:
php artisan make:middleware EnsureTokenIsValid这个命令会在 app/Http/Middleware 目录下生成一个新文件,里面已经写好了基本的中间件结构。
打开刚生成的 EnsureTokenIsValid.php,你会看到一个带 handle 方法的模板:
<?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)。如果要拦截请求,就直接返回响应或重定向。
中间件可以在处理请求前后都执行操作。
前置中间件(在请求到达控制器之前执行):
<?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
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。这样做集中了配置,也更好理解。
Laravel 12 中 bootstrap/app.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 闭包中用 append 或 prepend:
->withMiddleware(function ($middleware) {
$middleware->append(\App\Http\Middleware\EnsureTokenIsValid::class);
})append 会把中间件加到栈的末尾,prepend 则加到开头:
->withMiddleware(function ($middleware) {
$middleware->prepend(\App\Http\Middleware\LogRequests::class);
$middleware->append(\App\Http\Middleware\CompressResponse::class);
})路由中间件只作用于指定的路由或路由组。注册时需要给中间件起个别名,用 alias 方法:
->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,
]);
})中间件组可以把多个中间件打包在一起,方便批量应用:
->withMiddleware(function ($middleware) {
$middleware->appendToGroup('api', [
\App\Http\Middleware\EncryptCookies::class,
\Illuminate\Cookie\Middleware\AddQueuedCookiesToResponse::class,
]);
})Laravel 12 已经内置了 web 和 api 组。你可以向它们添加自己的中间件,也可以创建新的组。
注册好中间件后,有多种方式可以应用到路由上。
用 middleware 方法给单个路由加中间件:
use Illuminate\Support\Facades\Route;
Route::get('/dashboard', function () {
// Dashboard 逻辑
})->middleware('auth');传一个数组就可以同时应用多个:
Route::get('/admin/settings', function () {
// 管理员设置
})->middleware(['auth', 'admin', 'verified.email']);给一组路由加中间件:
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
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
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);
}
}用冒号把参数传给中间件:
Route::get('/admin/dashboard', function () {
// 仅限管理员
})->middleware('role:admin');
Route::get('/moderator/panel', function () {
// 仅限版主
})->middleware('role:moderator');也可以传多个参数,用逗号隔开:
Route::get('/content/edit', function () {
// 编辑内容
})->middleware('role:admin,editor');中间件会把它们当作独立的参数接收:
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
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
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 方法会在响应发出后才执行,所以不会影响用户体验。适合用来记日志、同步数据或做一些清理工作。
看几个在生产环境中常用的中间件实现。
<?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
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;
}
}<?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
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
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 12 内置了一些强大的中间件来处理常见任务:
跟着这些建议做,可以让你的中间件更好维护、更安全、性能也更好:
一个中间件只做一件事。如果一个中间件做的事情太多,就该拆分了:
// 坏例子:一个中间件做太多事
class HandleRequestMiddleware
{
public function handle($request, $next)
{
// 认证
// 验证
// 日志
// 转换数据
// 等等...
}
}
// 好例子:拆分成多个中间件
class AuthenticateMiddleware { }
class ValidateRequestMiddleware { }
class LogRequestMiddleware { }
class TransformRequestMiddleware { }让中间件接受参数,能提高复用性:
// 不要创建 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');中间件的顺序很重要。认证中间件应该放在需要用户信息的中间件之前:
Route::middleware(['auth', 'verified', 'role:admin'])->group(function () {
// 这里的路由
});不是所有逻辑都适合放在中间件里。中间件适合处理多个路由的通用逻辑。如果是特定路由的逻辑,考虑用:
中间件逻辑要尽量轻量。如果有耗时操作:
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());
}中间件里要做好异常处理,避免应用崩溃:
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
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 也能更好地提示:
use Illuminate\Http\Request;
use Symfony\Component\HttpFoundation\Response;
public function handle(Request $request, Closure $next): Response
{
// 实现
}如果中间件有参数,记得加注释说明:
/**
* 检查用户是否具有所需角色。
*
* @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
{
// 实现
}中间件在应用安全中很关键。注意这几点:
Laravel 的 VerifyCsrfToken 中间件要加入 web 组,用来保护所有修改数据的请求:
->withMiddleware(function ($middleware) {
$middleware->appendToGroup('web', [
\Illuminate\Foundation\Http\Middleware\VerifyCsrfToken::class,
]);
})API 路由要做好 Token 验证:
public function handle($request, $next)
{
$token = $request->bearerToken();
if (! $token || ! $this->isValidToken($token)) {
return response()->json(['error' => 'Unauthorized'], 401);
}
return $next($request);
}加上限流,防止被恶意攻击,特别是 API 和登录接口:
Route::middleware(['throttle:60,1'])->group(function () {
// 每分钟最多 60 次请求
});虽然 Laravel 已经有 CSRF 和 SQL 注入防护,但在中间件里再加一层清理也不错:
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);
}给敏感路由加多层中间件保护:
Route::middleware(['auth', 'verified', '2fa', 'role:admin'])->group(function () {
// 敏感的管理后台路由
});如果中间件不正常,试试这些方法:
public function handle($request, $next)
{
DB::enableQueryLog();
$response = $next($request);
Log::debug('Queries executed:', DB::getQueryLog());
return $response;
}public function handle($request, $next)
{
Log::debug('Middleware executed: ' . static::class);
return $next($request);
}Laravel Telescope 能帮你监控中间件执行、请求处理和性能指标。开发环境安装一下:
composer require laravel/telescope --dev
php artisan telescope:install
php artisan migrate什么时候用中间件,什么时候用其他特性?看这里:
这些方法能提升中间件性能:
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);
}不符合条件就赶紧返回,别继续执行:
public function handle($request, $next)
{
if (! $request->user()) {
return redirect('/login');
}
if (! $request->user()->isActive()) {
return response('Account suspended', 403);
}
return $next($request);
}不要一开始就加载所有依赖,用到再加载:
public function handle($request, $next)
{
// 仅在需要时加载服务
if ($request->has('validate')) {
app(ValidationService::class)->validate($request);
}
return $next($request);
}写中间件时避免这些坑:
必须 return $next($request) 的结果:
// 错误
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() 后再改请求已经没用了:
// 错误
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 里注册。
注意顺序,认证要放在权限检查之前:
// 错误的顺序
Route::middleware(['role:admin', 'auth'])
// 正确的顺序
Route::middleware(['auth', 'role:admin'])如果你是从 Laravel 11 升级来的,中间件注册方式改了:
protected $middleware = [
\App\Http\Middleware\TrustProxies::class,
];
protected $middlewareGroups = [
'web' => [
\App\Http\Middleware\EncryptCookies::class,
],
];
protected $routeMiddleware = [
'auth' => \App\Http\Middleware\Authenticate::class,
];->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 应用。
Kernel.php 移到了 bootstrap/app.php随着使用的深入,你会发现更多中间件的用法,也会形成自己的实现模式。记住一点:中间件要保持专注、逻辑清晰、文档齐全。