Laravel中间件执行顺序由注册位置决定,构成嵌套闭包链:越早注册越靠近外层,越早拦截请求、越晚处理响应;全局中间件包裹整个应用,路由中间件需显式绑定,数组顺序即执行顺序,且必须调用$next($request)才能继续链式执行。
$middleware、$middlewareGroups 和路由定义共同决定Laravel 不是按“全局 > 路由组 > 单个路由”线性叠加执行,而是把所有中间件构造成一个嵌套闭包链(类似俄罗斯套娃)。每次 next($request) 调用,实际是进入下一层中间件。所以顺序取决于你往链里“塞”的位置——越早注册的中间件,越靠近外层,也就越早拦截请求、越晚看到响应。
关键点:
$middleware(在 app/Http/Kernel.php)里的中间件会包裹整个应用,包括 Artisan 命令和非 HTTP 请求(如队列监听器触发的 HTTP 请求),但不包含 CLI 命令本身$middlewareGroups 中的 web 或 api 是命名分组,必须显式通过 ->middleware('web') 或 Route::middleware('api') 应用,不会自动生效->middleware(XXX) 时,该中间件会被插入到对应分组之后、控制器执行之前->middleware([First::class, Second::class]) 表示 First 先执行handle() 方法中忘记调用 $next($request) 就会中断请求链这是最常踩的坑:中间件不是“过滤器”,而是“拦截器 + 转发器”。如果你在 handle() 里做了权限判断后直接 return response()->json(...),那后续所有中间件和控制器都不会执行——这没错,但容易误以为“只是跳过”,其实整个链已终止。
正确做法是明确区分“放行”和“拦截”:
public function handle($request, Closure $next)
{
if (! $request->user() || ! $request->user()->hasRole('admin')) {
return response()->json(['error' => 'Unauthorized'], 403);
}
// ✅ 必须调用 $next 才能继续往下走
return $next($request);
}
漏掉 return $next($request) 的后果是:控制器永远收不到请求,且无报错,只有空白响应或超时。
session、csrf 等依赖有隐式顺序要求比如 StartSession 必须在 VerifyCsrfToken 之前运行,否则 CSRF token 读不到 session;而 EncryptCookies 又必须在 StartSession 之前,否则无法解密 session cookie。这些依赖关系不是靠文档记住的,而是看 $middleware 数组的书写顺序。
Laravel 默认顺序已经调好,但一旦你自定义中间件并加到 $middleware 开头或中间,就可能破坏这个链条。例如:
EncryptCookies 之前 → 读不到解密后的 cookieStartSession 之前 → $request->user() 为 nullapi 组里错误加入 StartSession → API 请求无状态,session 写入无效且可能引发异常路由绑定中间件时支持传参,但仅限简单值,语法是 middleware('role:admin,editor'),对应 handle($request, $next, ...$roles) 中的 $roles = ['admin', 'editor']。
注意限制:
'role:admin, editor' 会导致 $roles[1] 是 " editor")
middleware('throttle:60,1,true') 中最后一个 true 仍是字符串$request,而不是试图塞进参数参数本质是 Laravel 在解析路由时做的字符串切割,没有类型推断,也不经过 DI 容器。
中间件真正的复杂点不在写法,而在它横跨请求生命周期的两个方向:进入时可修改$request,退出时可修改 $response。很多人只关注“拦不拦”,却忘了返回的 $response 会被上游中间件再次处理——比如你在底层加了 X-Processed-By: MyMiddleware,但上层某个中间件又调用了 $response->withHeaders([]),这个 header 就丢了。