中间件
简介
中间件提供了一套方便机制,用来检查进入你应用的 HTTP 请求、对请求做过滤。比如 Laravel 自带了一个中间件,用来验证用户是否已登录——没登录就重定向到登录页,登录了就放行,让请求继续往应用深处走。
除了认证,你还可以写中间件做各种事。比如写一个日志中间件记录所有请求。Laravel 自带了认证、CSRF 保护等中间件;你自己写的中间件通常放在 app/Http/Middleware 目录下。
定义中间件
用 make:middleware Artisan 命令创建中间件:
php artisan make:middleware EnsureTokenIsValid
这会在 app/Http/Middleware 目录下生成一个 EnsureTokenIsValid 类。下面这个中间件只允许 token 输入值为 my-secret-token 的请求通过,否则重定向到 /home:
<?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');
}
return $next($request);
}
}
可以看到,如果 token 不匹配,中间件就返回一个重定向给客户端;匹配就把请求往下传。要让请求继续深入应用(也就是"放行"),就调用 $next 回调并传入 $request。
可以这样理解中间件:HTTP 请求在到达你的应用之前,要经过一系列"层"。每一层都可以检查请求,甚至可以把它直接拒绝掉。
提示
所有中间件都通过服务容器解析,所以你可以在中间件构造函数里用类型提示注入任何依赖。
(这句话如果看不懂,或者看了这句话你还是不知道依赖注入背后是怎么运转的,说明你没理解容器,需要回头看容器这篇,容器这个东西是要慢慢理解的, 容器和服务提供者是Laravel的核心,这两点不理解透彻的话, 是用不好这个框架的。)
中间件与响应
中间件可以在请求进入应用之前或之后执行任务。下面这个中间件在请求被应用处理之前干活:
<?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
{
// 执行操作
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);
// 执行操作
return $response;
}
}
注册中间件
全局中间件
如果你想让一个中间件在应用处理每一个 HTTP 请求时都运行,就把它追加到 bootstrap/app.php 的全局中间件栈里:
use App\Http\Middleware\EnsureTokenIsValid;
->withMiddleware(function (Middleware $middleware): void {
$middleware->append(EnsureTokenIsValid::class);
})
withMiddleware 闭包里拿到的 $middleware 对象是 Illuminate\Foundation\Configuration\Middleware 的实例,负责管理应用路由上分配的中间件。append 方法把中间件加到全局中间件列表的末尾;如果要加到开头,用 prepend 方法。
手动管理 Laravel 的默认全局中间件
如果你想手动管理 Laravel 的全局中间件栈,可以把 Laravel 默认的全局中间件列表传给 use 方法,然后按需调整:
->withMiddleware(function (Middleware $middleware): void {
$middleware->use([
\Illuminate\Foundation\Http\Middleware\InvokeDeferredCallbacks::class, // 执行延迟回调——提供者注册的引导后任务
\Illuminate\Http\Middleware\TrustHosts::class, //限制允许的 Host 头(防 Host 投毒攻击,默认关闭)
\Illuminate\Http\Middleware\TrustProxies::class, // 信任代理服务器,正确拿 scheme、ip、port(配 Nginx/Cloudflare 必须开)
\Illuminate\Http\Middleware\HandleCors::class, // 处理跨域请求头(Access-Control-*)
\Illuminate\Foundation\Http\Middleware\PreventRequestsDuringMaintenance::class, // 维护模式下拒绝请求(php artisan down 后生效)
\Illuminate\Http\Middleware\ValidatePostSize::class, // 拒绝超大 POST 请求体(防止内存耗尽)
\Illuminate\Foundation\Http\Middleware\TrimStrings::class, // 把请求参数首尾空格自动去掉(除了 password 等例外字段)
\Illuminate\Foundation\Http\Middleware\ConvertEmptyStringsToNull::class, // "" → null,统一空值处理
]);
})
注意:use() 会覆盖整个默认列表
$middleware->use() 不是"追加",而是全量替换。源码 getGlobalMiddleware() 里,你设了就直接用你的,不设才走默认列表。
文档示例漏掉了源码里排在第一位的 ValidatePathEncoding(拒绝 URL 编码异常的请求,防编码绕过攻击)。手动管理全局中间件时,除非你明确不需要它,否则加上:
\Illuminate\Http\Middleware\ValidatePathEncoding::class,
如果不确定要不要全量替换,更安全的做法是用 append / prepend 做增量修改。
关于 \Illuminate\Http\Middleware\TrustHosts::class, 默认即使这么开启也是不生效的,需要在 bootstrap/app.php 的 withMiddleware 里设置:
->withMiddleware(function (Middleware $middleware): void {
$middleware->trustHosts(); // 这样代表只信任 config('app.url') 中的域名+子域名
})
->withMiddleware(function (Middleware $middleware): void {
trustHosts(at: ['d.yeetu.com']) // 这样代表只信任指定的域名+子域名
})
->withMiddleware(function (Middleware $middleware): void {
trustHosts(at: ['d.yeetu.com'], subdomains: false) // 只信任指定域名,不含子域名
})
加trustHosts 的主要作用就是在于防止攻击者伪装 Host头,从而应用生成类似 https://evil.com/reset-password?token=xxx· 这种链接,发给用户就是钓鱼
关于\Illuminate\Http\Middleware\TrustProxies::class,默认是不信任任何代理的,要信任代理需要如下设置:
->withMiddleware(function (Middleware $middleware): void {
$middleware->trustProxies(at: '*');
})
at 参数控制信任哪些 IP,三种常见场景:
| 场景 | 配置 | 含义 |
|---|---|---|
| 代理和 Laravel 同机 | at: '*' | 信任 REMOTE_ADDR(就是 upstream Nginx 的 IP) |
| 知道代理的固定 IP | at: ['10.0.1.5', '10.0.1.0/24'] | 只信任这些 IP,其他来源的 X-Forwarded-* 头忽略 |
| 代理 IP 是动态的(如 AWS ALB) | at: '*' | 信任直接上游(ALB 的 IP),ALB 自己会管好安全 |
headers 参数一般不用改——默认已经信任了所有标准的 X-Forwarded-* 头(For、Proto、Host、Port、Prefix、AWS ELB),覆盖了 99% 的场景。
如果你的Laravel只跑在一台服务器上,没有做负载均衡,Docker等,也就是说Laravel 收到的请求是"原始客户端直连"的,中间没有经过了一道 HTTP 转发,那么就不用去设置 $middleware->trustProxies(at: '*')了。
给路由分配中间件
如果只想给特定路由分配中间件,在定义路由时调 middleware 方法:
use App\Http\Middleware\EnsureTokenIsValid;
Route::get('/profile', function () {
// ...
})->middleware(EnsureTokenIsValid::class);
传数组可以一次分配多个:
Route::get('/', function () {
// ...
})->middleware([First::class, Second::class]);
移除中间件
给一组路由分配了中间件后,如果想让其中某一条路由不走这个中间件,用 withoutMiddleware:
use App\Http\Middleware\EnsureTokenIsValid;
Route::middleware([EnsureTokenIsValid::class])->group(function () {
Route::get('/', function () {
// ...
});
Route::get('/profile', function () {
// ...
})->withoutMiddleware([EnsureTokenIsValid::class]);
});
也可以给一整组路由定义排除中间件:
use App\Http\Middleware\EnsureTokenIsValid;
Route::withoutMiddleware([EnsureTokenIsValid::class])->group(function () {
Route::get('/profile', function () {
// ...
});
});
withoutMiddleware 只能移除路由中间件,对全局中间件无效。
中间件组
有时想把几个中间件打个包,用一个 key 来引用,方便批量分配。在 bootstrap/app.php 里用 appendToGroup 方法:
use App\Http\Middleware\First;
use App\Http\Middleware\Second;
->withMiddleware(function (Middleware $middleware): void {
$middleware->appendToGroup('group-name', [ // 注意:appendToGroup 只是注册,这里面的中间件是不会全局生效的
First::class,
Second::class,
]);
$middleware->prependToGroup('group-name', [
First::class,
Second::class,
]);
})
中间件组可以像单个中间件一样分配给路由和控制器的 action:
Route::get('/', function () {
// ...
})->middleware('group-name');
Route::middleware(['group-name'])->group(function () {
// ...
});
Laravel 的默认中间件组
Laravel 预定义了 web 和 api 两个中间件组,分别包含了 Web 路由和 API 路由上常用的中间件。Laravel 会自动把它们应用到对应的 routes/web.php 和 routes/api.php 文件上:
web 中间件组 |
|---|
Illuminate\Cookie\Middleware\EncryptCookies |
Illuminate\Cookie\Middleware\AddQueuedCookiesToResponse |
Illuminate\Session\Middleware\StartSession |
Illuminate\View\Middleware\ShareErrorsFromSession |
Illuminate\Foundation\Http\Middleware\PreventRequestForgery |
Illuminate\Routing\Middleware\SubstituteBindings |
api 中间件组 |
|---|
Illuminate\Routing\Middleware\SubstituteBindings |
如果要往这些组里追加或前置中间件,在 bootstrap/app.php 里用 web 和 api 方法——这两个方法本质上是 appendToGroup 的快捷方式:
use App\Http\Middleware\EnsureTokenIsValid;
use App\Http\Middleware\EnsureUserIsSubscribed;
->withMiddleware(function (Middleware $middleware): void {
$middleware->web(append: [
EnsureUserIsSubscribed::class,
]);
$middleware->api(prepend: [
EnsureTokenIsValid::class,
]);
})
你甚至可以替换掉 Laravel 默认中间件组里的某项,换成自己的中间件:
use App\Http\Middleware\StartCustomSession;
use Illuminate\Session\Middleware\StartSession;
$middleware->web(replace: [
StartSession::class => StartCustomSession::class,
]);
或者干脆删掉某个中间件:
$middleware->web(remove: [
StartSession::class,
]);
手动管理 Laravel 的默认中间件组
如果你想完全手动掌控 web 和 api 中间件组里的所有中间件,可以重新定义这两个组。下面的例子展示了两个组的默认中间件配置,你可以按需改动:
->withMiddleware(function (Middleware $middleware): void {
$middleware->group('web', [
\Illuminate\Cookie\Middleware\EncryptCookies::class,
\Illuminate\Cookie\Middleware\AddQueuedCookiesToResponse::class,
\Illuminate\Session\Middleware\StartSession::class,
\Illuminate\View\Middleware\ShareErrorsFromSession::class,
\Illuminate\Foundation\Http\Middleware\PreventRequestForgery::class,
\Illuminate\Routing\Middleware\SubstituteBindings::class,
// \Illuminate\Session\Middleware\AuthenticateSession::class,
]);
$middleware->group('api', [
// \Laravel\Sanctum\Http\Middleware\EnsureFrontendRequestsAreStateful::class,
// 'throttle:api',
\Illuminate\Routing\Middleware\SubstituteBindings::class,
]);
})
提示
默认情况下,web 和 api 中间件组会由 bootstrap/app.php 自动应用到 routes/web.php 和 routes/api.php。
注意
如果开启了 'throttle:api', 需要到AppServiceProvider::boot() 里定义api的具体限流逻辑, 没有写这段的话,即使开启'throttle:api,也是没有限流的
// 方式 1:命名限流器(灵活,支持 after 回调等高级功能)
RateLimiter::for('api', function (Request $request) {
return Limit::perMinute(60)->by($request->ip());
});
// 方式 2:原始数字(简单粗暴,不需要额外定义)
->middleware('throttle:60,1') // 60次/分钟,按 IP 分
中间件别名
你可以在 bootstrap/app.php 里给中间件起别名。中间件别名让你给某个中间件类定义一个短名,对那些类名特别长的中间件尤其好用:
use App\Http\Middleware\EnsureUserIsSubscribed;
->withMiddleware(function (Middleware $middleware): void {
$middleware->alias([
'subscribed' => EnsureUserIsSubscribed::class
]);
})
定义了别名之后,就可以用别名来分配中间件:
Route::get('/profile', function () {
// ...
})->middleware('subscribed');
为了方便,Laravel 内置的一些中间件已经有了默认别名。比如 auth 就是 Illuminate\Auth\Middleware\Authenticate 的别名。下面是所有默认中间件别名:
| 别名 | 中间件 |
|---|---|
auth |
Illuminate\Auth\Middleware\Authenticate |
auth.basic |
Illuminate\Auth\Middleware\AuthenticateWithBasicAuth |
auth.session |
Illuminate\Session\Middleware\AuthenticateSession |
cache.headers |
Illuminate\Http\Middleware\SetCacheHeaders |
can |
Illuminate\Auth\Middleware\Authorize |
guest |
Illuminate\Auth\Middleware\RedirectIfAuthenticated |
password.confirm |
Illuminate\Auth\Middleware\RequirePassword |
precognitive |
Illuminate\Foundation\Http\Middleware\HandlePrecognitiveRequests |
signed |
Illuminate\Routing\Middleware\ValidateSignature |
subscribed |
\Spark\Http\Middleware\VerifyBillableIsSubscribed |
throttle |
Illuminate\Routing\Middleware\ThrottleRequests 或 Illuminate\Routing\Middleware\ThrottleRequestsWithRedis |
verified |
Illuminate\Auth\Middleware\EnsureEmailIsVerified |
中间件排序
少数情况下,你可能需要中间件按特定顺序执行,但又没法控制它们在路由上被分配的顺序。这时可以在 bootstrap/app.php 里用 priority 方法指定优先级:
->withMiddleware(function (Middleware $middleware): void {
$middleware->priority([
\Illuminate\Foundation\Http\Middleware\HandlePrecognitiveRequests::class,
\Illuminate\Cookie\Middleware\EncryptCookies::class,
\Illuminate\Cookie\Middleware\AddQueuedCookiesToResponse::class,
\Illuminate\Session\Middleware\StartSession::class,
\Illuminate\View\Middleware\ShareErrorsFromSession::class,
\Illuminate\Foundation\Http\Middleware\PreventRequestForgery::class,
\Laravel\Sanctum\Http\Middleware\EnsureFrontendRequestsAreStateful::class,
\Illuminate\Routing\Middleware\ThrottleRequests::class,
\Illuminate\Routing\Middleware\ThrottleRequestsWithRedis::class,
\Illuminate\Routing\Middleware\SubstituteBindings::class,
\Illuminate\Contracts\Auth\Middleware\AuthenticatesRequests::class,
\Illuminate\Auth\Middleware\Authorize::class,
]);
})
中间件参数
中间件也可以接收额外参数。比如你的应用需要验证当前登录用户是否具备某个"角色"才能执行操作,就可以写一个 EnsureUserHasRole 中间件,把角色名作为额外参数传进去。
额外参数会在 $next 参数之后传给中间件的 handle 方法:
<?php
namespace App\Http\Middleware;
use Closure;
use Illuminate\Http\Request;
use Symfony\Component\HttpFoundation\Response;
class EnsureUserHasRole
{
/**
* Handle an incoming request.
*
* @param \Closure(\Illuminate\Http\Request): (\Symfony\Component\HttpFoundation\Response) $next
*/
public function handle(Request $request, Closure $next, string $role): Response
{
if (! $request->user()->hasRole($role)) {
// 重定向...
}
return $next($request);
}
}
定义路由时,用 : 分隔中间件名和参数:
use App\Http\Middleware\EnsureUserHasRole;
Route::put('/post/{id}', function (string $id) {
// ...
})->middleware(EnsureUserHasRole::class.':editor');
多个参数用逗号分隔:
Route::put('/post/{id}', function (string $id) {
// ...
})->middleware(EnsureUserHasRole::class.':editor,publisher');
可终止中间件
有时中间件需要在 HTTP 响应已经发送到浏览器之后再做些收尾工作。如果你在中间件里定义了 terminate 方法,并且 Web 服务器用的是 FastCGI,terminate 方法会在响应发送后自动被调用:
<?php
namespace Illuminate\Session\Middleware;
use Closure;
use Illuminate\Http\Request;
use Symfony\Component\HttpFoundation\Response;
class TerminatingMiddleware
{
/**
* Handle an incoming request.
*
* @param \Closure(\Illuminate\Http\Request): (\Symfony\Component\HttpFoundation\Response) $next
*/
public function handle(Request $request, Closure $next): Response
{
return $next($request);
}
/**
* Handle tasks after the response has been sent to the browser.
*/
public function terminate(Request $request, Response $response): void
{
// ...
}
}
terminate 方法接收 $request 和 $response 两个参数。定义好可终止中间件后,把它加到 bootstrap/app.php 的路由列表或全局中间件中即可。
调用中间件的 terminate 方法时,Laravel 会从服务容器中解析一个全新的中间件实例。如果你想让 handle 和 terminate 用的是同一个实例,就在容器里用 singleton 方法注册这个中间件。通常在 AppServiceProvider 的 register 方法里做这件事:
// AppServiceProvider.php
use App\Http\Middleware\TerminatingMiddleware;
/**
* Register any application services.
*/
public function register(): void
{
$this->app->singleton(TerminatingMiddleware::class);
}
什么时候用可终止中间件?什么时候要 singleton?
下面的场景你可以用终止中间件
使用场景:需要在响应已经发给用户之后执行的收尾工作——记录访问日志、上报分析数据、清理临时文件等。这些工作不该拖慢用户体验,所以放到 terminate 里异步化。
为什么默认创建新实例? handle 执行过程中中间件可能修改了内部属性(计数、缓存等),复用同一个实例会导致 terminate 拿到被污染的状态。新实例保证收尾逻辑有个干净的起点,这是默认安全策略。
什么时候必须注册 singleton? 当 terminate 需要访问 handle 阶段产生的数据。比如 Laravel 的 StartSession 中间件:handle 里启动会话、往 session 里写数据,terminate 里把同一份 session 持久化到 Redis / 数据库。如果用新实例,session 数据就全丢了。
实际开发
这个终止中间件其实开发中可以说不用,比如说记录日志这些工作,使用队列会更好,这个东西其实是给Laravel框架内部自己用的。开发中就直接别用好了