PHP中间件本质是接收$request和$next、返回$response的函数或方法,核心在于正确调用$next($request)并确保响应流不被破坏;所有框架中间件都必须遵守此契约,否则链式调用中断。

PHP中间件不是框架的专利,也不是必须用 Composer 才能写的“高级功能”——它就是一个接收 $request 和 $next、返回 $response 的函数或方法。能不能跑起来,只取决于你有没有在正确时机调用 $next($request),以及有没有提前输出内容破坏响应流。
中间件必须接收两个参数并返回响应
所有 PHP 中间件(无论 Laravel、ThinkPHP 还是手写)都得满足这个契约:输入是请求数据($request)和下一个可调用项($next),输出必须是响应($response)。漏掉 return、返回 null 或直接 echo/exit,链就断了。
- 错误写法:
function($request, $next) { if (!auth()) die('401'); $next($request); }—— 没有return,后续中间件收不到响应 - 正确写法:
function($request, $next) { if (!auth()) return response('401', 401); return $next($request); } - ThinkPHP 中类形式中间件,
handle()方法也必须有return $next($request),否则控制器永远不执行
Laravel 中间件注册顺序决定执行逻辑
中间件按数组顺序从上到下进入“洋葱皮”,再从下到上返回。顺序错,轻则日志记了非法请求,重则鉴权被绕过。
- 全局中间件写在
app/Http/Kernel.php的$middleware数组里,所有请求都走 - 鉴权中间件必须放在限流中间件之前:如果先限流再鉴权,未登录用户也能耗尽配额
- CORS 中间件要覆盖
OPTIONS请求,不能只对GET/POST生效;Laravel 自带的HandleCors默认不启用,得手动加进$middleware - 路由级中间件如
->middleware('auth:api'),别名必须在app/Http/Kernel.php的$routeMiddleware里注册,否则报Class not found
手写 pipe 调度器时 body 只能读一次
原生 PHP 或 Swoole 环境下自己写中间件链,php://input 和 $request->getContent() 都只能读取一次。前面中间件读了,后面就为空——这不是 bug,是底层限制。
立即学习“PHP免费学习笔记(深入)”;
- 解决方案:在第一个中间件(比如解析 JSON 的那个)里统一读取并缓存到
$request对象属性中,例如$request->rawBody = file_get_contents('php://input'); - 别在多个中间件里重复调
json_decode(file_get_contents('php://input')),第二次会返回null - Swoole 中更要注意:用
$request->rawContent()替代file_get_contents('php://input'),前者是框架封装的缓存读取 - JWT 解析中间件若需验证签名,公钥必须从文件或环境变量加载,绝不能硬编码在代码里
OPTIONS 预检请求必须显式处理
前端带 credentials: true 时,浏览器会先发 OPTIONS 请求。如果中间件没拦截它,直接交给路由,就会 404 或触发鉴权失败——而你根本看不到日志,因为预检失败不进业务逻辑。
- 正确做法:在鉴权中间件开头加判断
if ($request->getMethod() === 'OPTIONS') { return response('', 204)->withHeader('Access-Control-Allow-Origin', 'https://example.com'); } - Access-Control-Allow-Credentials: true 时,
Access-Control-Allow-Origin不能是*,必须写具体域名 - 限流中间件也要对 OPTIONS 生效——高频 OPTIONS 是常见攻击手段,不能放行后才限流
真正难的不是写中间件,而是想清楚哪一层该做什么:鉴权不能依赖 Session 尚未启动的中间件,日志不能记录被短路掉的请求,CORS 头不能只在响应里加而不处理预检。这些边界条件,比语法更决定中间件是否可靠。



















