跳至內容

中介層

簡介

中介層提供了一種方便的機制來檢查和過濾進入您的應用程式的 HTTP 請求。例如,Laravel 包含一個中介層,用於驗證您應用程式的使用者是否已通過驗證。如果使用者未通過驗證,則中介層會將使用者重新導向到您應用程式的登入畫面。但是,如果使用者已通過驗證,則中介層會允許請求進一步進入應用程式。

除了驗證之外,還可以編寫其他中介層來執行各種任務。例如,日誌中介層可能會記錄所有進入您應用程式的請求。Laravel 中包含各種中介層,包括用於驗證和 CSRF 保護的中介層;但是,所有使用者定義的中介層通常都位於您應用程式的 app/Http/Middleware 目錄中。

定義中介層

若要建立新的中介層,請使用 make:middleware Artisan 命令

php artisan make:middleware EnsureTokenIsValid

此命令會將新的 EnsureTokenIsValid 類別放置在您的 app/Http/Middleware 目錄中。在此中介層中,如果提供的 token 輸入與指定的值相符,我們才會允許存取該路由。否則,我們會將使用者重新導向回 /home URI

<?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 與我們的密碼權杖不符,中介層會向用戶端傳回 HTTP 重新導向;否則,請求會進一步傳遞到應用程式中。若要將請求更深入地傳遞到應用程式中(允許中介層「通過」),您應該使用 $request 呼叫 $next 回呼。

最好將中介層想像成一系列 HTTP 請求必須通過才能到達您的應用程式的「層」。每一層都可以檢查請求,甚至完全拒絕它。

lightbulb

所有中介層都會透過服務容器解析,因此您可以在中介層的建構子中鍵入您需要的任何依賴項。

中介層和回應

當然,中介層可以在將請求更深入地傳遞到應用程式之前或之後執行任務。例如,以下中介層會在應用程式處理請求之前執行某些任務

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

但是,此中介層會在應用程式處理請求之後執行其任務

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

您可以將多個中介層指派給路由,方法是將中介層名稱的陣列傳遞給 middleware 方法

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 包含預先定義的 webapi 中介層群組,其中包含您可能想要套用至網頁和 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 方法的方便替代方案

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 預設 webapi 中介層群組中的所有中介層,您可以完全重新定義群組。以下範例會使用其預設中介層定義 webapi 中介層群組,讓您可以根據需要自訂它們

->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 檔案中將別名指派給中介層。中介層別名可讓您為給定的中介層類別定義簡短的別名,這對於具有長類別名稱的中介層特別有用

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 中介層的別名。以下是預設中介層別名的清單

別名 中介層
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
訪客 Illuminate\Auth\Middleware\RedirectIfAuthenticated
密碼確認 Illuminate\Auth\Middleware\RequirePassword
預知請求 Illuminate\Foundation\Http\Middleware\HandlePrecognitiveRequests
已簽署 Illuminate\Routing\Middleware\ValidateSignature
已訂閱 \Spark\Http\Middleware\VerifyBillableIsSubscribed
流量限制 Illuminate\Routing\Middleware\ThrottleRequestsIlluminate\Routing\Middleware\ThrottleRequestsWithRedis
已驗證 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 方法,並且您的網路伺服器正在使用 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 將從 服務容器 解析該中間件的一個新實例。如果您希望在呼叫 handleterminate 方法時使用相同的中間件實例,請使用容器的 singleton 方法將該中間件註冊到容器中。通常,這應該在您的 AppServiceProviderregister 方法中完成。

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