中间件
介绍
中间件提供了一种方便的机制来检查和过滤进入应用程序的 HTTP 请求。例如,Laravel 包含一个中间件来验证应用程序的用户是否已认证。如果用户未认证,中间件会将用户重定向到应用程序的登录界面。然而,如果用户已认证,中间件将允许请求进一步进入应用程序。
除了认证之外,还可以编写其他中间件来执行各种任务。例如,日志中间件可能会记录所有进入应用程序的请求。Laravel 包含多种中间件,包括用于认证和 CSRF 保护的中间件;然而,所有用户定义的中间件通常位于应用程序的 app/Http/Middleware
目录中。
定义中间件
要创建新的中间件,请使用 make:middleware
Artisan 命令:
php artisan make:middleware EnsureTokenIsValid
此命令将在 app/Http/Middleware
目录中放置一个新的 EnsureTokenIsValid
类。在这个中间件中,我们将仅允许访问路由,如果提供的 token
输入与指定的值匹配。否则,我们将用户重定向回 /home
URI:
<?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.
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
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
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
文件中将其添加到全局中间件堆栈中:
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
方法。然后,你可以根据需要调整默认的中间件堆栈:
->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
方法:
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
方法只能移除路由中间件,对全局中间件不起作用。
中间件组
有时你可能想要将多个中间件组合在一个键下,使它们更容易分配给路由。你可以在应用程序的 bootstrap/app.php
文件中使用 appendToGroup
方法来实现这一点:
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,
]);
})
中间件组可以使用与单个中间件相同的语法分配给路由和控制器操作:
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\ValidateCsrfToken |
Illuminate\Routing\Middleware\SubstituteBindings |
api 中间件组 |
---|
Illuminate\Routing\Middleware\SubstituteBindings |
如果你想要向这些组添加或预置中间件,可以在应用程序的 bootstrap/app.php
文件中使用 web
和 api
方法。web
和 api
方法是 appendToGroup
方法的便捷替代方案:
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 默认中间件组中的某个条目:
use App\Http\Middleware\StartCustomSession;
use Illuminate\Session\Middleware\StartSession;
$middleware->web(replace: [
StartSession::class => StartCustomSession::class,
]);
或者,你可以完全移除某个中间件:
$middleware->web(remove: [
StartSession::class,
]);
手动管理 Laravel 的默认中间件组
如果你想手动管理 Laravel 默认的 web
和 api
中间件组中的所有中间件,你可以完全重新定义这些组。下面的示例将使用它们的默认中间件定义 web
和 api
中间件组,允许你根据需要自定义它们:
->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,
]);
})
默认情况下,web
和 api
中间件组会由 bootstrap/app.php
文件自动应用到应用程序相应的 routes/web.php
和 routes/api.php
文件。
中间件别名
你可以在应用程序的 bootstrap/app.php
文件中为中间件分配别名。中间件别名允许你为给定的中间件类定义一个简短的别名,这对于类名较长的中间件特别有用:
use App\Http\Middleware\EnsureUserIsSubscribed;
->withMiddleware(function (Middleware $middleware) {
$middleware->alias([
'subscribed' => EnsureUserIsSubscribed::class
]);
})
一旦在应用程序的 bootstrap/app.php
文件中定义了中间件别名,你就可以在为路由分配中间件时使用该别名:
Route::get('/profile', function () {
// ...
})->middleware('subscribed');
为了方便起见,Laravel 的一些内置中间件默认已设置了别名。例如,auth
中间件是 Illuminate\Auth\Middleware\Authenticate
中间件的别名。以下是默认中间件别名的列表:
Alias | Middleware |
---|---|
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 or Illuminate\Routing\Middleware\ThrottleRequestsWithRedis |
verified | Illuminate\Auth\Middleware\EnsureEmailIsVerified |
中间件排序
在某些罕见情况下,你可能需要中间件按特定顺序执行,但在将它们分配给路由时无法控制其顺序。在这些情况下,你可以在应用程序的 bootstrap/app.php
文件中使用 priority
方法指定中间件优先级:
->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
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);
}
}
在定义路由时,可以通过用 :
分隔中间件名称和参数来指定中间件参数:
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
方法应该同时接收请求和响应。一旦你定义了一个可终止的中间件,你应该将其添加到应用程序的 bootstrap/app.php
文件中的路由列表或全局中间件中。
当在你的中间件上调用 terminate
方法时,Laravel 将从服务容器中解析一个新的中间件实例。如果你想在调用 handle
和 terminate
方法时使用相同的中间件实例,请使用容器的 singleton
方法在容器中注册中间件。这通常应该在你的 AppServiceProvider
的 register
方法中完成:
use App\Http\Middleware\TerminatingMiddleware;
/**
* Register any application services.
*/
public function register(): void
{
$this->app->singleton(TerminatingMiddleware::class);
}