中介軟體 - Middleware
簡介
Middleware 提供了一個機制,可檢驗與過濾進入應用程式的 HTTP Request。舉例來說,Laravel 中包含了一個可以認證使用者是否已登入的 Middleware。若使用者未登入,該 Middleware 會將使用者重新導向回登入畫面。不過,若使用者已登入,這個 Middleware 就會讓 Request 進一步進入程式中處理。
除了登入認證外,我們還能撰寫追加的 Middleware 來進行各種任務。舉例來說,可以有個 Logging Middleware 來將程式的所有連入 Request 都紀錄到日誌裡。Laravel Framework 還包含了許多 Middleware,包含用於登入認證的 Middleware、以及用於 CSRF 保護的 Middleware。這些 Middleware 都放置在 app/Http/Middleware
目錄內。
定義 Middleware
若要建立新的 Middleware,請使用 make:middleware
Artisan 指令:
1php artisan make:middleware EnsureTokenIsValid
1php artisan make:middleware EnsureTokenIsValid
該指令會在 app/Http/Middleware
目錄中放置一個新的 EnsureTokenIsValid
類別。在這個 Middleware 中,我們要只在提供的 token
符合特定的值時才允許存取該 Route。token
不符合時,會將使用者重新導向回到 home
URI:
1<?php23namespace App\Http\Middleware;45use Closure;6use Illuminate\Http\Request;7use Symfony\Component\HttpFoundation\Response;89class EnsureTokenIsValid10{11 /**12 * Handle an incoming request.13 *14 * @param \Closure(\Illuminate\Http\Request): (\Symfony\Component\HttpFoundation\Response) $next15 */16 public function handle(Request $request, Closure $next): Response17 {18 if ($request->input('token') !== 'my-secret-token') {19 return redirect('home');20 }2122 return $next($request);23 }24}
1<?php23namespace App\Http\Middleware;45use Closure;6use Illuminate\Http\Request;7use Symfony\Component\HttpFoundation\Response;89class EnsureTokenIsValid10{11 /**12 * Handle an incoming request.13 *14 * @param \Closure(\Illuminate\Http\Request): (\Symfony\Component\HttpFoundation\Response) $next15 */16 public function handle(Request $request, Closure $next): Response17 {18 if ($request->input('token') !== 'my-secret-token') {19 return redirect('home');20 }2122 return $next($request);23 }24}
就像我們可以看到的一樣,若給定的 token
不符合我們的私密權杖 (Secret Token),則這個 Middleware 會回傳一個 HTTP Redirect 給用戶端。token
符合時,這個 Request 就會進一步地傳給我們的程式。若要將 Request 進一步傳進我們的應用程式中 (即,讓 Middleware「通過 - Pass」),應以 $request
呼叫 $next
回呼。
最好想像成我們有「一層又一層」的 Middleware。HTTP Request 必須通過每一層的 Middleware,最後才能進入你的應用程式中。每一層 Middleware 都可以檢查 Request 的內容,甚至還能完全拒絕 Request。
所有的 Middleware 都會經過 [Service Container] 解析,因此我們可以在 Middleware 的 Constructor (建構函式) 上型別提示 (Type-Hint) 任何需要的相依性。
Middleware and Responses
當然,Middleware 可以在將 Request 傳入應用程式的前後執行。舉例來說,下列 Middleware 會在 Request 被程式處理 之後 進行一些任務:
1<?php23namespace App\Http\Middleware;45use Closure;6use Illuminate\Http\Request;7use Symfony\Component\HttpFoundation\Response;89class BeforeMiddleware10{11 public function handle(Request $request, Closure $next): Response12 {13 // Perform action1415 return $next($request);16 }17}
1<?php23namespace App\Http\Middleware;45use Closure;6use Illuminate\Http\Request;7use Symfony\Component\HttpFoundation\Response;89class BeforeMiddleware10{11 public function handle(Request $request, Closure $next): Response12 {13 // Perform action1415 return $next($request);16 }17}
不過,這個 Middleware 會在 Request 被程式處理 之後 才進行其任務:
1<?php23namespace App\Http\Middleware;45use Closure;6use Illuminate\Http\Request;7use Symfony\Component\HttpFoundation\Response;89class AfterMiddleware10{11 public function handle(Request $request, Closure $next): Response12 {13 $response = $next($request);1415 // Perform action1617 return $response;18 }19}
1<?php23namespace App\Http\Middleware;45use Closure;6use Illuminate\Http\Request;7use Symfony\Component\HttpFoundation\Response;89class AfterMiddleware10{11 public function handle(Request $request, Closure $next): Response12 {13 $response = $next($request);1415 // Perform action1617 return $response;18 }19}
註冊 Middleware
全域 Middleware
若想讓 Middleware 在每一個 HTTP Request 上都執行的話,請將該 Middleware 列在 app/Http/Kernel.php
類別中的 $middleware
屬性內。
Assigning Middleware to Routes
若想將 Middleware 指定到特定的 Route 中,可以在定義 Route 時呼叫 middleware
方法:
1use App\Http\Middleware\Authenticate;23Route::get('/profile', function () {4 // ...5})->middleware(Authenticate::class);
1use App\Http\Middleware\Authenticate;23Route::get('/profile', function () {4 // ...5})->middleware(Authenticate::class);
也可以傳入一組 Middleware 陣列給 middleware
方法來指派多個 Middleware 給 Route:
1Route::get('/', function () {2 // ...3})->middleware([First::class, Second::class]);
1Route::get('/', function () {2 // ...3})->middleware([First::class, Second::class]);
為了讓指定 Middleware 更簡單,也可以在專案的 app/Http/Kernel.php
檔案中為這些 Middleware 指定別名。預設情況下,該類別的 $middlewareAliases
屬性內包含了 Laravel 內建的一些 Middleware。可以根據需求自行在該屬性內為你的 Middleware 加上別名:
1// Within App\Http\Kernel class...23protected $middlewareAliases = [4 'auth' => \App\Http\Middleware\Authenticate::class,5 'auth.basic' => \Illuminate\Auth\Middleware\AuthenticateWithBasicAuth::class,6 'bindings' => \Illuminate\Routing\Middleware\SubstituteBindings::class,7 'cache.headers' => \Illuminate\Http\Middleware\SetCacheHeaders::class,8 'can' => \Illuminate\Auth\Middleware\Authorize::class,9 'guest' => \App\Http\Middleware\RedirectIfAuthenticated::class,10 'signed' => \Illuminate\Routing\Middleware\ValidateSignature::class,11 'throttle' => \Illuminate\Routing\Middleware\ThrottleRequests::class,12 'verified' => \Illuminate\Auth\Middleware\EnsureEmailIsVerified::class,13];
1// Within App\Http\Kernel class...23protected $middlewareAliases = [4 'auth' => \App\Http\Middleware\Authenticate::class,5 'auth.basic' => \Illuminate\Auth\Middleware\AuthenticateWithBasicAuth::class,6 'bindings' => \Illuminate\Routing\Middleware\SubstituteBindings::class,7 'cache.headers' => \Illuminate\Http\Middleware\SetCacheHeaders::class,8 'can' => \Illuminate\Auth\Middleware\Authorize::class,9 'guest' => \App\Http\Middleware\RedirectIfAuthenticated::class,10 'signed' => \Illuminate\Routing\Middleware\ValidateSignature::class,11 'throttle' => \Illuminate\Routing\Middleware\ThrottleRequests::class,12 'verified' => \Illuminate\Auth\Middleware\EnsureEmailIsVerified::class,13];
在 HTTP Kernel 中定義好 Middleware 別名後,就可以使用這些別名來在 Route 上指定 Middleware:
1Route::get('/profile', function () {2 // ...3})->middleware('auth');
1Route::get('/profile', function () {2 // ...3})->middleware('auth');
排除 Middleware
當我們將 Middleware 指派給 Route 群組時,我們有時候會需要讓某個 Middleware 不要被套用到群組中的個別 Route 上。我們可以使用 withoutMiddleware
方法來完成:
1use App\Http\Middleware\EnsureTokenIsValid;23Route::middleware([EnsureTokenIsValid::class])->group(function () {4 Route::get('/', function () {5 // ...6 });78 Route::get('/profile', function () {9 // ...10 })->withoutMiddleware([EnsureTokenIsValid::class]);11});
1use App\Http\Middleware\EnsureTokenIsValid;23Route::middleware([EnsureTokenIsValid::class])->group(function () {4 Route::get('/', function () {5 // ...6 });78 Route::get('/profile', function () {9 // ...10 })->withoutMiddleware([EnsureTokenIsValid::class]);11});
也可以將一組 Middleware 從整個 Route 群組定義中排除:
1use App\Http\Middleware\EnsureTokenIsValid;23Route::withoutMiddleware([EnsureTokenIsValid::class])->group(function () {4 Route::get('/profile', function () {5 // ...6 });7});
1use App\Http\Middleware\EnsureTokenIsValid;23Route::withoutMiddleware([EnsureTokenIsValid::class])->group(function () {4 Route::get('/profile', function () {5 // ...6 });7});
withoutMiddleware
方法只能移除 Route Middleware,不能移除全域 Middleware。
Middleware 群組
有時候,我們會想將多個 Middleware 分組在單一索引鍵上,來讓我們可以輕鬆地將其指派給 Route。可以在 HTTP Kernel 中使用 $middlewareGroups
屬性來完成。
Laravel 中包含了預先定義的 web
與 api
兩個 Middleware 群組,其中共包含了可用在網頁與 API Route 上的常見 Middleware。請記得,這些 Middleware 群組由 App\Providers\RouteServiceProvider
Service Provider 自動套用到對應的 web
與 api
Route 檔案:
1/**2 * The application's route middleware groups.3 *4 * @var array5 */6protected $middlewareGroups = [7 'web' => [8 \App\Http\Middleware\EncryptCookies::class,9 \Illuminate\Cookie\Middleware\AddQueuedCookiesToResponse::class,10 \Illuminate\Session\Middleware\StartSession::class,11 \Illuminate\View\Middleware\ShareErrorsFromSession::class,12 \App\Http\Middleware\VerifyCsrfToken::class,13 \Illuminate\Routing\Middleware\SubstituteBindings::class,14 ],1516 'api' => [17 \Illuminate\Routing\Middleware\ThrottleRequests::class.':api',18 \Illuminate\Routing\Middleware\SubstituteBindings::class,19 ],20];
1/**2 * The application's route middleware groups.3 *4 * @var array5 */6protected $middlewareGroups = [7 'web' => [8 \App\Http\Middleware\EncryptCookies::class,9 \Illuminate\Cookie\Middleware\AddQueuedCookiesToResponse::class,10 \Illuminate\Session\Middleware\StartSession::class,11 \Illuminate\View\Middleware\ShareErrorsFromSession::class,12 \App\Http\Middleware\VerifyCsrfToken::class,13 \Illuminate\Routing\Middleware\SubstituteBindings::class,14 ],1516 'api' => [17 \Illuminate\Routing\Middleware\ThrottleRequests::class.':api',18 \Illuminate\Routing\Middleware\SubstituteBindings::class,19 ],20];
也可以使用相同的語法來將 Middleware 群組作為個別 Middleware 一樣指派給 Route 與 Controller 動作。同樣的,使用 Middleware 群組來一次指派多個 Middleware 給 Route 比較方便:
1Route::get('/', function () {2 // ...3})->middleware('web');45Route::middleware(['web'])->group(function () {6 // ...7});
1Route::get('/', function () {2 // ...3})->middleware('web');45Route::middleware(['web'])->group(function () {6 // ...7});
在新安裝的 Laravel 中隨附了 web
與 api
Middleware 群組,並由 App\Providers\RouteServiceProvider
自動套用到對應的 routes/web.php
與 routes/api.php
檔上。
排序 Middleware
我們偶爾會需要讓 Middleware 以特定的順序執行,但有時候沒有辦法控制 Middleware 是以什麼順序指派給 Route 的。這時,我們可以使用 app/Http/Kernel.php
檔案中的 $middlewarePriority
屬性來執行 Middleware 的優先順序。這個屬性預設可能不存在 HTTP Kernel 中。若沒有這個屬性,可以複製下列預設定義來用:
1/**2 * The priority-sorted list of middleware.3 *4 * This forces non-global middleware to always be in the given order.5 *6 * @var string[]7 */8protected $middlewarePriority = [9 \Illuminate\Foundation\Http\Middleware\HandlePrecognitiveRequests::class,10 \Illuminate\Cookie\Middleware\EncryptCookies::class,11 \Illuminate\Cookie\Middleware\AddQueuedCookiesToResponse::class,12 \Illuminate\Session\Middleware\StartSession::class,13 \Illuminate\View\Middleware\ShareErrorsFromSession::class,14 \Illuminate\Contracts\Auth\Middleware\AuthenticatesRequests::class,15 \Illuminate\Routing\Middleware\ThrottleRequests::class,16 \Illuminate\Routing\Middleware\ThrottleRequestsWithRedis::class,17 \Illuminate\Contracts\Session\Middleware\AuthenticatesSessions::class,18 \Illuminate\Routing\Middleware\SubstituteBindings::class,19 \Illuminate\Auth\Middleware\Authorize::class,20];
1/**2 * The priority-sorted list of middleware.3 *4 * This forces non-global middleware to always be in the given order.5 *6 * @var string[]7 */8protected $middlewarePriority = [9 \Illuminate\Foundation\Http\Middleware\HandlePrecognitiveRequests::class,10 \Illuminate\Cookie\Middleware\EncryptCookies::class,11 \Illuminate\Cookie\Middleware\AddQueuedCookiesToResponse::class,12 \Illuminate\Session\Middleware\StartSession::class,13 \Illuminate\View\Middleware\ShareErrorsFromSession::class,14 \Illuminate\Contracts\Auth\Middleware\AuthenticatesRequests::class,15 \Illuminate\Routing\Middleware\ThrottleRequests::class,16 \Illuminate\Routing\Middleware\ThrottleRequestsWithRedis::class,17 \Illuminate\Contracts\Session\Middleware\AuthenticatesSessions::class,18 \Illuminate\Routing\Middleware\SubstituteBindings::class,19 \Illuminate\Auth\Middleware\Authorize::class,20];
Middleware 參數
Middleware 也可以接收額外的參數。舉例來說,若你的程式需要在執行給定動作前認證登入的使用者是否有給定的「職位 (Role)」,則我們可以先建立一個 EnsureUserHasRole
Middleware,讓該 Middleware 接收一個職位名稱來作為其額外的引數。
額外的 Middleware 引數會被放在 $next
引數之後傳遞給 Middleware:
1<?php23namespace App\Http\Middleware;45use Closure;6use Illuminate\Http\Request;7use Symfony\Component\HttpFoundation\Response;89class EnsureUserHasRole10{11 /**12 * Handle an incoming request.13 *14 * @param \Closure(\Illuminate\Http\Request): (\Symfony\Component\HttpFoundation\Response) $next15 */16 public function handle(Request $request, Closure $next, string $role): Response17 {18 if (! $request->user()->hasRole($role)) {19 // Redirect...20 }2122 return $next($request);23 }2425}
1<?php23namespace App\Http\Middleware;45use Closure;6use Illuminate\Http\Request;7use Symfony\Component\HttpFoundation\Response;89class EnsureUserHasRole10{11 /**12 * Handle an incoming request.13 *14 * @param \Closure(\Illuminate\Http\Request): (\Symfony\Component\HttpFoundation\Response) $next15 */16 public function handle(Request $request, Closure $next, string $role): Response17 {18 if (! $request->user()->hasRole($role)) {19 // Redirect...20 }2122 return $next($request);23 }2425}
Middleware parameters may be specified when defining the route by separating the middleware name and parameters with a :
:
1Route::put('/post/{id}', function (string $id) {2 // ...3})->middleware('role:editor');
1Route::put('/post/{id}', function (string $id) {2 // ...3})->middleware('role:editor');
Multiple parameters may be delimited by commas:
1Route::put('/post/{id}', function (string $id) {2 // ...3})->middleware('role:editor,publisher');
1Route::put('/post/{id}', function (string $id) {2 // ...3})->middleware('role:editor,publisher');
可終止的 Middleware
有時候,某個 Middleware 可能需要在 HTTP Response 被傳送到瀏覽器後才進行某些動作。若我們在 Middleware 上定義一個 terminate
方法,且網頁伺服器 (Web Server) 使用 FastCGI,則會在 Response 傳送給瀏覽器後會自動呼叫 terminate
方法:
1<?php23namespace Illuminate\Session\Middleware;45use Closure;6use Illuminate\Http\Request;7use Symfony\Component\HttpFoundation\Response;89class TerminatingMiddleware10{11 /**12 * Handle an incoming request.13 *14 * @param \Closure(\Illuminate\Http\Request): (\Symfony\Component\HttpFoundation\Response) $next15 */16 public function handle(Request $request, Closure $next): Response17 {18 return $next($request);19 }2021 /**22 * Handle tasks after the response has been sent to the browser.23 */24 public function terminate(Request $request, Response $response): void25 {26 // ...27 }28}
1<?php23namespace Illuminate\Session\Middleware;45use Closure;6use Illuminate\Http\Request;7use Symfony\Component\HttpFoundation\Response;89class TerminatingMiddleware10{11 /**12 * Handle an incoming request.13 *14 * @param \Closure(\Illuminate\Http\Request): (\Symfony\Component\HttpFoundation\Response) $next15 */16 public function handle(Request $request, Closure $next): Response17 {18 return $next($request);19 }2021 /**22 * Handle tasks after the response has been sent to the browser.23 */24 public function terminate(Request $request, Response $response): void25 {26 // ...27 }28}
terminate
方法應接收 Request 與 Response。定義好可終止的 Middleware (Terminable Middleware) 後,請將其加到 Route 列表或 app/Http/Kernel.php
檔案中的全域 Middleware 內。
呼叫 Middleware 上的 terminate
方法時,Laravel 會從 [Service Container] 中解析出這個 Middleware 的新實體。若想讓 handle
與 terminate
都在同一個 Middleware 實體上呼叫的話,請使用 Container 的 singleton
方法來想 Container 註冊這個 Middleware。一般來說,這個註冊應在 AppServiceProvider
的 register
方法中進行:
1use App\Http\Middleware\TerminatingMiddleware;23/**4 * Register any application services.5 */6public function register(): void7{8 $this->app->singleton(TerminatingMiddleware::class);9}
1use App\Http\Middleware\TerminatingMiddleware;23/**4 * Register any application services.5 */6public function register(): void7{8 $this->app->singleton(TerminatingMiddleware::class);9}