15分鐘搞懂中介層(Middleware)

簡介
中介層提供了一種方便的機制來監測與過濾進入應用的 HTTP 請求。例如,Laravel 包含一個驗證用戶身份的中介層。 如果用戶未能通過認證,中介層會把用戶轉址到登入頁面。 反之,用戶如果通過驗證, 中介層將把請求進一步轉發到應用
當然中介層除了驗證身份外還可以編寫來執行各種任務。例如:CORS 中介層可以負責為所有的應用返回的 responses 添加合適的 Header。日誌中介層可以記錄所有傳入應用的請求
Laravel 自帶了一些中介層,包括身份驗證、CSRF 保護等。所有的中介層都位於 app/Http/Middleware 資料夾內
定義中介層
你可以使用 make:middleware 來創建一個中介層:
php artisan make:middleware EnsureTokenIsValid
該命令會在 app/Http/Middleware 資料夾內放置新的 EnsureTokenIsValid 類別。在這個中介層中,我們僅允許 token 輸入值符合特定值的請求對路由進行訪問,否則將轉址到 home 頁面:
//app\Http\Middleware\EnsureTokenIsValid.php
<?php
namespace App\\Http\\Middleware;
use Closure;
class EnsureTokenIsValid
{
/**
* Handle an incoming request.
*
* @param \\Illuminate\\Http\\Request $request
* @param \\Closure $next
* @return mixed
*/
public function handle($request, Closure $next)
{
if ($request->input('token') !== 'my-secret-token') {
return redirect('home');
}
return $next($request);
}
}
正如您所見,如果提供的 token 參數不符合我們的私鑰, 這個中介層將返回一個 HTTP 轉址給客戶端;否則這個請求將會通過,進一步傳遞到應用層中。要讓請求繼續傳到應用層中 (即允許 「通過」中介層驗證), 只需要將 $request 作為參數來呼叫函數 $next 即可
可以將中介層想像成一層又一層的濾紙,HTTP 請求必須通過它們才能進入你的應用層。每一層中介層都會檢查請求是否它的要求,而後決定讓請求通過或拒絕訪問應用
技巧: 所有的中介層都是透過服務容器來解析的,因此,你可以在中介層建構子中以型別提示鍵入你需要的任何依賴
中介層 & 回應
當然,中介層可以在請求深入應用之前或之後去執行工作。比如下面這個例子中介層將在請求被應用處理之前去執行一些任務:
//app\Http\Middleware\BeforeMiddleware.php
<?php
namespace App\\Http\\Middleware;
use Closure;
class BeforeMiddleware
{
public function handle($request, Closure $next)
{
// Perform action
return $next($request);
}
}
然而,如果這樣寫的話,中介層就會改在應用處理請求之後才執行工作:
//app\Http\Middleware\AfterMiddleware.php
<?php
namespace App\\Http\\Middleware;
use Closure;
class AfterMiddleware
{
public function handle($request, Closure $next)
{
$response = $next($request);
// Perform action
return $response;
}
}
註冊中介層
全域中介層
如果你希望某個中介層在應用處理每個 HTTP 請求期間都要運行, 只需要在 app\Http\Kernel.php 中的 $middleware 屬性中加入這個中介層
分配中介層給路由
假設你想為指定的路由分配中介層 , 首先應該在 app\Http\Kernel.php 檔案內為該中介層設定一個鍵。預設情況下,該類別中的 $routeMiddleware 屬性下包含了 Laravel 所內建的中介層。若要加入自定義的中介層,只需把它附加到列表後並為其分配一個自定義鍵如下:
//App\Http\Kernel.php
protected $routeMiddleware = [
'auth' => \\App\\Http\\Middleware\\Authenticate::class,
'auth.basic' => \\Illuminate\\Auth\\Middleware\\AuthenticateWithBasicAuth::class,
'bindings' => \\Illuminate\\Routing\\Middleware\\SubstituteBindings::class,
'cache.headers' => \\Illuminate\\Http\\Middleware\\SetCacheHeaders::class,
'can' => \\Illuminate\\Auth\\Middleware\\Authorize::class,
'guest' => \\App\\Http\\Middleware\\RedirectIfAuthenticated::class,
'signed' => \\Illuminate\\Routing\\Middleware\\ValidateSignature::class,
'throttle' => \\Illuminate\\Routing\\Middleware\\ThrottleRequests::class,
'verified' => \\Illuminate\\Auth\\Middleware\\EnsureEmailIsVerified::class, //你自定義的中介層
];
一旦你把中介層加入了 HTTP 內核,你就能使用 middleware() 來將中介層分配給路由:
//routes\web.php
Route::get('/profile', function () {
//
})->middleware('auth');
你也可以一次分配多個中介層給路由,作法是在 middleware() 內傳入陣列,裡頭放的是中介層的鍵
//routes\web.php
Route::get('/', function () {
//
})->middleware(['first', 'second']);
如果你不想要用鍵來指定,也可以直接在 middleware() 傳入中介層的完整類別名稱:
//routes\web.php
use App\\Http\\Middleware\\EnsureTokenIsValid;
Route::get('/profile', function () {
//
})->middleware(EnsureTokenIsValid::class);
當中介層已被分配給某個路由群組,但你需要將某個特定路由從這個中介層被排除掉,這時你就需要用到 withoutMiddleware() :
//routes\web.php
use App\\Http\\Middleware\\EnsureTokenIsValid;
Route::middleware([EnsureTokenIsValid::class])->group(function () {
Route::get('/', function () {
//
});
Route::get('/profile', function () {
//
})->withoutMiddleware([EnsureTokenIsValid::class]);
});
注意: withoutMiddleware() 只能移除 route 中介層而不能移除全域中介層
中介層群組
有時,你可能會希望將多個中介層歸屬為同一個鍵,以使其更易於分配給路由。 你可以使用 HTTP 內核的 $middlewareGroups 屬性來達成
Laravel 預設就帶有 web 和 api 這兩個中介層群組,其中包含一般要應用於 Web 和 API 路由的泛用型中介層。別忘了,這些中介層群組將自動的被 App\Providers\RouteServiceProvider 服務供應器分配給寫在 web 和 api 路由檔裡的所有路由:
//app\Http\Kernel.php
/**
* The application's route middleware groups.
*
* @var array
*/
protected $middlewareGroups = [
'web' => [
\\App\\Http\\Middleware\\EncryptCookies::class,
\\Illuminate\\Cookie\\Middleware\\AddQueuedCookiesToResponse::class,
\\Illuminate\\Session\\Middleware\\StartSession::class,
// \\Illuminate\\Session\\Middleware\\AuthenticateSession::class,
\\Illuminate\\View\\Middleware\\ShareErrorsFromSession::class,
\\App\\Http\\Middleware\\VerifyCsrfToken::class,
\\Illuminate\\Routing\\Middleware\\SubstituteBindings::class,
],
'api' => [
'throttle:api',
\\Illuminate\\Routing\\Middleware\\SubstituteBindings::class,
],
];
中介層群組同樣可以分配給路由和控制器方法,使用的也是和分配單一中介層相同的語法:
//routes/web.php
Route::get('/', function () {
//
})->middleware('web');
Route::middleware(['web'])->group(function () {
//
});
注意: Laravel的預設行為就會自動的把 web 和 api 這兩個中介層群組分配給你 routes/web.php 和 routes/api.php 這兩個檔案裡頭的路由,透過 App\Providers\RouteServiceProvider.php 檔案
排序中介層
極少情況下,你可能需要中介層以特定的順序來執行,但是當它們被分配到路由時,你無法控制它們的順序。在這種情況下,可以利用 app/Http/Kernel.php 檔案中的 $middlewarePriority 屬性來指定中介層的優先級,越前面的中介層會先被執行。這個屬性預設可能不在 HTTP內核裡頭,你可以複製這裡的預設內容如下:
//app\Http\Kernel.php
/**
* The priority-sorted list of middleware.
*
* This forces non-global middleware to always be in the given order.
*
* @var array
*/
protected $middlewarePriority = [
\\Illuminate\\Cookie\\Middleware\\EncryptCookies::class,
\\Illuminate\\Session\\Middleware\\StartSession::class,
\\Illuminate\\View\\Middleware\\ShareErrorsFromSession::class,
\\Illuminate\\Contracts\\Auth\\Middleware\\AuthenticatesRequests::class,
\\Illuminate\\Routing\\Middleware\\ThrottleRequests::class,
\\Illuminate\\Session\\Middleware\\AuthenticateSession::class,
\\Illuminate\\Routing\\Middleware\\SubstituteBindings::class,
\\Illuminate\\Auth\\Middleware\\Authorize::class,
];
中介層參數
中介層還可以接收額外的參數。例如,如果你的應用程序需要在執行特定操作之前先驗證用戶是否為指定的「角色」, 你可以創建一個 EnsureUserHasRole 中介層,由它來接收「角色」名稱作為附加參數
附加的中介層參數會在 $next 參數之後傳遞給中介層:
//app\Http\Middleware\EnsureUserHasRole.php
<?php
namespace App\\Http\\Middleware;
use Closure;
class EnsureUserHasRole
{
/**
* Handle the incoming request.
*
* @param \\Illuminate\\Http\\Request $request
* @param \\Closure $next
* @param string $role
* @return mixed
*/
public function handle($request, Closure $next, $role)
{
if (! $request->user()->hasRole($role)) {
// Redirect...
}
return $next($request);
}
}
定義路由時,如需分配帶參數的中介層可通過一個 : 來隔開中介層名稱和參數。如有多個參數就使用逗號分隔:
//routes\web.php
Route::put('/post/{id}', function ($id) {
//
})->middleware('role:editor');
Terminal 中介層
有時可能需要在 HTTP 回應給瀏覽器之後做一些事情。 如果你在中介層定義了一個 terminate 方法,並且你的網頁伺服器使用的是 FastCGI,那麼 terminate 方法會在回應發送到瀏覽器之後自動被呼叫:
<?php
namespace Illuminate\\Session\\Middleware;
use Closure;
class TerminatingMiddleware
{
/**
* 處理傳入的回應.
*
* @param \\Illuminate\\Http\\Request $request
* @param \\Closure $next
* @return mixed
*/
public function handle($request, Closure $next)
{
return $next($request);
}
/**
* 在回應傳回到瀏覽器之後要作的事情.
*
* @param \\Illuminate\\Http\\Request $request
* @param \\Illuminate\\Http\\Response $response
* @return void
*/
public function terminate($request, $response)
{
// ...
}
}
這個 terminate() 需要接受 request 和 response。一旦你定義了一個帶有 terminable() 的中介層,就需要把它加入 app/Http/Kernel.php 檔案的 $routeMiddleware 或者是 $middleware 當中
當你在中介層上呼叫 terminate() 的時候,Laravel 將從服務容器中解析出一個新的中介層實例。如果希望在呼叫 handle() 和 terminate() 時使用相同的中介層實例, 請使用容器的 singleton() 來註冊中介層, 通常這會寫在 AppServiceProvider.php 檔案中的 register() :
//app\Providers\AppServiceProvider.php
use App\\Http\\Middleware\\TerminatingMiddleware;
/**
* Register any application services.
*
* @return void
*/
public function register()
{
$this->app->singleton(TerminatingMiddleware::class);
}