Skip to content

中间件

介绍

中间件提供了一种方便的机制来检查和过滤进入应用程序的 HTTP 请求。例如,Laravel 包含一个中间件来验证应用程序的用户是否已认证。如果用户未认证,中间件会将用户重定向到应用程序的登录界面。然而,如果用户已认证,中间件将允许请求进一步进入应用程序。

除了认证之外,还可以编写其他中间件来执行各种任务。例如,日志中间件可能会记录所有进入应用程序的请求。Laravel 包含多种中间件,包括用于认证和 CSRF 保护的中间件;然而,所有用户定义的中间件通常位于应用程序的 app/Http/Middleware 目录中。

定义中间件

要创建新的中间件,请使用 make:middleware Artisan 命令:

shell
php artisan make:middleware EnsureTokenIsValid

此命令将在 app/Http/Middleware 目录中放置一个新的 EnsureTokenIsValid 类。在这个中间件中,我们将仅允许访问路由,如果提供的 token 输入与指定的值匹配。否则,我们将用户重定向回 /home URI:

php
<?php

namespace App\Http\Middleware;

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

class EnsureTokenIsValid
{
    /**
     * 处理传入的请求。
     *
     * @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);
    }
}

As you can see, if the given token does not match our secret token, the middleware will return an HTTP redirect to the client; otherwise, the request will be passed further into the application. To pass the request deeper into the application (allowing the middleware to "pass"), you should call the $next callback with the $request.

It's best to envision middleware as a series of "layers" HTTP requests must pass through before they hit your application. Each layer can examine the request and even reject it entirely.

lightbulb

All middleware are resolved via the service container, so you may type-hint any dependencies you need within a middleware's constructor.

Middleware and Responses

Of course, a middleware can perform tasks before or after passing the request deeper into the application. For example, the following middleware would perform some task before the request is handled by the application:

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
    {
        // Perform action

        return $next($request);
    }
}

However, this middleware would perform its task after the request is handled by the application:

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);

        // Perform action

        return $response;
    }
}

注册中间件

全局中间件

如果你希望在应用程序的每个 HTTP 请求期间运行中间件,可以在应用程序的 bootstrap/app.php 文件中将其添加到全局中间件堆栈中:

php
use App\Http\Middleware\EnsureTokenIsValid;

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

提供给 withMiddleware 闭包的 $middleware 对象是 Illuminate\Foundation\Configuration\Middleware 的实例,负责管理分配给应用程序路由的中间件。append 方法将中间件添加到全局中间件列表的末尾。如果你想将中间件添加到列表的开头,应该使用 prepend 方法。

手动管理 Laravel 的默认全局中间件

如果你想手动管理 Laravel 的全局中间件堆栈,可以将 Laravel 的默认全局中间件堆栈提供给 use 方法。然后,你可以根据需要调整默认的中间件堆栈:

php
->withMiddleware(function (Middleware $middleware) {
    $middleware->use([
        \Illuminate\Foundation\Http\Middleware\InvokeDeferredCallbacks::class,
        // \Illuminate\Http\Middleware\TrustHosts::class,
        \Illuminate\Http\Middleware\TrustProxies::class,
        \Illuminate\Http\Middleware\HandleCors::class,
        \Illuminate\Foundation\Http\Middleware\PreventRequestsDuringMaintenance::class,
        \Illuminate\Http\Middleware\ValidatePostSize::class,
        \Illuminate\Foundation\Http\Middleware\TrimStrings::class,
        \Illuminate\Foundation\Http\Middleware\ConvertEmptyStringsToNull::class,
    ]);
})

为路由分配中间件

如果你想为特定路由分配中间件,可以在定义路由时调用 middleware 方法:

php
use App\Http\Middleware\EnsureTokenIsValid;

Route::get('/profile', function () {
    // ...
})->middleware(EnsureTokenIsValid::class);

你可以通过传递中间件名称数组来为路由分配多个中间件:

php
Route::get('/', function () {
    // ...
})->middleware([First::class, Second::class]);

排除中间件

当为一组路由分配中间件时,你可能偶尔需要阻止中间件应用于组内的某个单独路由。你可以使用 withoutMiddleware 方法来实现这一点:

php
use App\Http\Middleware\EnsureTokenIsValid;

Route::middleware([EnsureTokenIsValid::class])->group(function () {
    Route::get('/', function () {
        // ...
    });

    Route::get('/profile', function () {
        // ...
    })->withoutMiddleware([EnsureTokenIsValid::class]);
});

你也可以从整个路由组中排除一组给定的中间件:

php
use App\Http\Middleware\EnsureTokenIsValid;

Route::withoutMiddleware([EnsureTokenIsValid::class])->group(function () {
    Route::get('/profile', function () {
        // ...
    });
});

withoutMiddleware 方法只能移除路由中间件,对全局中间件不起作用。

中间件组

有时你可能想要将多个中间件组合在一个键下,使它们更容易分配给路由。你可以在应用程序的 bootstrap/app.php 文件中使用 appendToGroup 方法来实现这一点:

php
use App\Http\Middleware\First;
use App\Http\Middleware\Second;

->withMiddleware(function (Middleware $middleware) {
    $middleware->appendToGroup('group-name', [
        First::class,
        Second::class,
    ]);

    $middleware->prependToGroup('group-name', [
        First::class,
        Second::class,
    ]);
})

中间件组可以使用与单个中间件相同的语法分配给路由和控制器操作:

php
Route::get('/', function () {
    // ...
})->middleware('group-name');

Route::middleware(['group-name'])->group(function () {
    // ...
});

Laravel 的默认中间件组

Laravel 包含预定义的 webapi 中间件组,其中包含了你可能想要应用到 Web 和 API 路由的常用中间件。请记住,Laravel 会自动将这些中间件组应用到相应的 routes/web.phproutes/api.php 文件:

web 中间件组
Illuminate\Cookie\Middleware\EncryptCookies
Illuminate\Cookie\Middleware\AddQueuedCookiesToResponse
Illuminate\Session\Middleware\StartSession
Illuminate\View\Middleware\ShareErrorsFromSession
Illuminate\Foundation\Http\Middleware\ValidateCsrfToken
Illuminate\Routing\Middleware\SubstituteBindings
api 中间件组
Illuminate\Routing\Middleware\SubstituteBindings

如果你想要向这些组添加或预置中间件,可以在应用程序的 bootstrap/app.php 文件中使用 webapi 方法。webapi 方法是 appendToGroup 方法的便捷替代方案:

php
use App\Http\Middleware\EnsureTokenIsValid;
use App\Http\Middleware\EnsureUserIsSubscribed;

->withMiddleware(function (Middleware $middleware) {
    $middleware->web(append: [
        EnsureUserIsSubscribed::class,
    ]);

    $middleware->api(prepend: [
        EnsureTokenIsValid::class,
    ]);
})

你甚至可以用自己的自定义中间件替换 Laravel 默认中间件组中的某个条目:

php
use App\Http\Middleware\StartCustomSession;
use Illuminate\Session\Middleware\StartSession;

$middleware->web(replace: [
    StartSession::class => StartCustomSession::class,
]);

或者,你可以完全移除某个中间件:

php
$middleware->web(remove: [
    StartSession::class,
]);

手动管理 Laravel 的默认中间件组

如果你想手动管理 Laravel 默认的 webapi 中间件组中的所有中间件,你可以完全重新定义这些组。下面的示例将使用它们的默认中间件定义 webapi 中间件组,允许你根据需要自定义它们:

php
->withMiddleware(function (Middleware $middleware) {
    $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\ValidateCsrfToken::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,
    ]);
})
lightbulb

默认情况下,webapi 中间件组会由 bootstrap/app.php 文件自动应用到应用程序相应的 routes/web.phproutes/api.php 文件。

中间件别名

你可以在应用程序的 bootstrap/app.php 文件中为中间件分配别名。中间件别名允许你为给定的中间件类定义一个简短的别名,这对于类名较长的中间件特别有用:

php
use App\Http\Middleware\EnsureUserIsSubscribed;

->withMiddleware(function (Middleware $middleware) {
    $middleware->alias([
        'subscribed' => EnsureUserIsSubscribed::class
    ]);
})

一旦在应用程序的 bootstrap/app.php 文件中定义了中间件别名,你就可以在为路由分配中间件时使用该别名:

php
Route::get('/profile', function () {
    // ...
})->middleware('subscribed');

为了方便起见,Laravel 的一些内置中间件默认已设置了别名。例如,auth 中间件是 Illuminate\Auth\Middleware\Authenticate 中间件的别名。以下是默认中间件别名的列表:

AliasMiddleware
authIlluminate\Auth\Middleware\Authenticate
auth.basicIlluminate\Auth\Middleware\AuthenticateWithBasicAuth
auth.sessionIlluminate\Session\Middleware\AuthenticateSession
cache.headersIlluminate\Http\Middleware\SetCacheHeaders
canIlluminate\Auth\Middleware\Authorize
guestIlluminate\Auth\Middleware\RedirectIfAuthenticated
password.confirmIlluminate\Auth\Middleware\RequirePassword
precognitiveIlluminate\Foundation\Http\Middleware\HandlePrecognitiveRequests
signedIlluminate\Routing\Middleware\ValidateSignature
subscribed\Spark\Http\Middleware\VerifyBillableIsSubscribed
throttleIlluminate\Routing\Middleware\ThrottleRequests or Illuminate\Routing\Middleware\ThrottleRequestsWithRedis
verifiedIlluminate\Auth\Middleware\EnsureEmailIsVerified

中间件排序

在某些罕见情况下,你可能需要中间件按特定顺序执行,但在将它们分配给路由时无法控制其顺序。在这些情况下,你可以在应用程序的 bootstrap/app.php 文件中使用 priority 方法指定中间件优先级:

php
->withMiddleware(function (Middleware $middleware) {
    $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\ValidateCsrfToken::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 参数之后传递给中间件:

php
<?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)) {
            // Redirect...
        }

        return $next($request);
    }

}

在定义路由时,可以通过用 : 分隔中间件名称和参数来指定中间件参数:

php
use App\Http\Middleware\EnsureUserHasRole;

Route::put('/post/{id}', function (string $id) {
    // ...
})->middleware(EnsureUserHasRole::class.':editor');

多个参数可以用逗号分隔:

php
Route::put('/post/{id}', function (string $id) {
    // ...
})->middleware(EnsureUserHasRole::class.':editor,publisher');

可终止的中间件

有时中间件可能需要在 HTTP 响应发送到浏览器之后执行一些工作。如果你在中间件上定义了 terminate 方法,并且你的 web 服务器使用 FastCGI,那么 terminate 方法将在响应发送到浏览器后自动被调用:

php
<?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 方法应该同时接收请求和响应。一旦你定义了一个可终止的中间件,你应该将其添加到应用程序的 bootstrap/app.php 文件中的路由列表或全局中间件中。

当在你的中间件上调用 terminate 方法时,Laravel 将从服务容器中解析一个新的中间件实例。如果你想在调用 handleterminate 方法时使用相同的中间件实例,请使用容器的 singleton 方法在容器中注册中间件。这通常应该在你的 AppServiceProviderregister 方法中完成:

php
use App\Http\Middleware\TerminatingMiddleware;

/**
 * Register any application services.
 */
public function register(): void
{
    $this->app->singleton(TerminatingMiddleware::class);
}