中介軟體 - 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<?php
2 
3namespace App\Http\Middleware;
4 
5use Closure;
6use Illuminate\Http\Request;
7use Symfony\Component\HttpFoundation\Response;
8 
9class EnsureTokenIsValid
10{
11 /**
12 * Handle an incoming request.
13 *
14 * @param \Closure(\Illuminate\Http\Request): (\Symfony\Component\HttpFoundation\Response) $next
15 */
16 public function handle(Request $request, Closure $next): Response
17 {
18 if ($request->input('token') !== 'my-secret-token') {
19 return redirect('home');
20 }
21 
22 return $next($request);
23 }
24}
1<?php
2 
3namespace App\Http\Middleware;
4 
5use Closure;
6use Illuminate\Http\Request;
7use Symfony\Component\HttpFoundation\Response;
8 
9class EnsureTokenIsValid
10{
11 /**
12 * Handle an incoming request.
13 *
14 * @param \Closure(\Illuminate\Http\Request): (\Symfony\Component\HttpFoundation\Response) $next
15 */
16 public function handle(Request $request, Closure $next): Response
17 {
18 if ($request->input('token') !== 'my-secret-token') {
19 return redirect('home');
20 }
21 
22 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。

lightbulb

所有的 Middleware 都會經過 [Service Container] 解析,因此我們可以在 Middleware 的 Constructor (建構函式) 上型別提示 (Type-Hint) 任何需要的相依性。

Middleare 與 Response

當然,Middleware 可以在將 Request 傳入應用程式的前後執行。舉例來說,下列 Middleware 會在 Request 被程式處理 之後 進行一些任務:

1<?php
2 
3namespace App\Http\Middleware;
4 
5use Closure;
6use Illuminate\Http\Request;
7use Symfony\Component\HttpFoundation\Response;
8 
9class BeforeMiddleware
10{
11 public function handle(Request $request, Closure $next): Response
12 {
13 // 進行動作
14 
15 return $next($request);
16 }
17}
1<?php
2 
3namespace App\Http\Middleware;
4 
5use Closure;
6use Illuminate\Http\Request;
7use Symfony\Component\HttpFoundation\Response;
8 
9class BeforeMiddleware
10{
11 public function handle(Request $request, Closure $next): Response
12 {
13 // 進行動作
14 
15 return $next($request);
16 }
17}

不過,這個 Middleware 會在 Request 被程式處理 之後 才進行其任務:

1<?php
2 
3namespace App\Http\Middleware;
4 
5use Closure;
6use Illuminate\Http\Request;
7use Symfony\Component\HttpFoundation\Response;
8 
9class AfterMiddleware
10{
11 public function handle(Request $request, Closure $next): Response
12 {
13 $response = $next($request);
14 
15 // 進行動作
16 
17 return $response;
18 }
19}
1<?php
2 
3namespace App\Http\Middleware;
4 
5use Closure;
6use Illuminate\Http\Request;
7use Symfony\Component\HttpFoundation\Response;
8 
9class AfterMiddleware
10{
11 public function handle(Request $request, Closure $next): Response
12 {
13 $response = $next($request);
14 
15 // 進行動作
16 
17 return $response;
18 }
19}

註冊 Middleware

全域 Middleware

若想讓 Middleware 在每一個 HTTP Request 上都執行的話,請將該 Middleware 列在 app/Http/Kernel.php 類別中的 $middleware 屬性內。

將 Middleware 指派給 Route

若想將 Middleware 指定到特定的 Route 中,可以在定義 Route 時呼叫 middleware 方法:

1use App\Http\Middleware\Authenticate;
2 
3Route::get('/profile', function () {
4 // ...
5})->middleware(Authenticate::class);
1use App\Http\Middleware\Authenticate;
2 
3Route::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// 在 App\Http\Kernel 類別中...
2 
3protected $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// 在 App\Http\Kernel 類別中...
2 
3protected $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;
2 
3Route::middleware([EnsureTokenIsValid::class])->group(function () {
4 Route::get('/', function () {
5 // ...
6 });
7 
8 Route::get('/profile', function () {
9 // ...
10 })->withoutMiddleware([EnsureTokenIsValid::class]);
11});
1use App\Http\Middleware\EnsureTokenIsValid;
2 
3Route::middleware([EnsureTokenIsValid::class])->group(function () {
4 Route::get('/', function () {
5 // ...
6 });
7 
8 Route::get('/profile', function () {
9 // ...
10 })->withoutMiddleware([EnsureTokenIsValid::class]);
11});

也可以將一組 Middleware 從整個 Route 群組定義中排除:

1use App\Http\Middleware\EnsureTokenIsValid;
2 
3Route::withoutMiddleware([EnsureTokenIsValid::class])->group(function () {
4 Route::get('/profile', function () {
5 // ...
6 });
7});
1use App\Http\Middleware\EnsureTokenIsValid;
2 
3Route::withoutMiddleware([EnsureTokenIsValid::class])->group(function () {
4 Route::get('/profile', function () {
5 // ...
6 });
7});

withoutMiddleware 方法只能移除 Route Middleware,不能移除全域 Middleware

Middleware 群組

有時候,我們會想將多個 Middleware 分組在單一索引鍵上,來讓我們可以輕鬆地將其指派給 Route。可以在 HTTP Kernel 中使用 $middlewareGroups 屬性來完成。

Laravel 中包含了預先定義的 webapi 兩個 Middleware 群組,其中共包含了可用在網頁與 API Route 上的常見 Middleware。請記得,這些 Middleware 群組由 App\Providers\RouteServiceProvider Service Provider 自動套用到對應的 webapi Route 檔案:

1/**
2 * The application's route middleware groups.
3 *
4 * @var array
5 */
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 ],
15 
16 '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 array
5 */
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 ],
15 
16 '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');
4 
5Route::middleware(['web'])->group(function () {
6 // ...
7});
1Route::get('/', function () {
2 // ...
3})->middleware('web');
4 
5Route::middleware(['web'])->group(function () {
6 // ...
7});
lightbulb

在新安裝的 Laravel 中隨附了 webapi Middleware 群組,並由 App\Providers\RouteServiceProvider 自動套用到對應的 routes/web.phproutes/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\Session\Middleware\StartSession::class,
12 \Illuminate\View\Middleware\ShareErrorsFromSession::class,
13 \Illuminate\Contracts\Auth\Middleware\AuthenticatesRequests::class,
14 \Illuminate\Routing\Middleware\ThrottleRequests::class,
15 \Illuminate\Routing\Middleware\ThrottleRequestsWithRedis::class,
16 \Illuminate\Contracts\Session\Middleware\AuthenticatesSessions::class,
17 \Illuminate\Routing\Middleware\SubstituteBindings::class,
18 \Illuminate\Auth\Middleware\Authorize::class,
19];
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\Session\Middleware\StartSession::class,
12 \Illuminate\View\Middleware\ShareErrorsFromSession::class,
13 \Illuminate\Contracts\Auth\Middleware\AuthenticatesRequests::class,
14 \Illuminate\Routing\Middleware\ThrottleRequests::class,
15 \Illuminate\Routing\Middleware\ThrottleRequestsWithRedis::class,
16 \Illuminate\Contracts\Session\Middleware\AuthenticatesSessions::class,
17 \Illuminate\Routing\Middleware\SubstituteBindings::class,
18 \Illuminate\Auth\Middleware\Authorize::class,
19];

Middleware 參數

Middleware 也可以接收額外的參數。舉例來說,若你的程式需要在執行給定動作前認證登入的使用者是否有給定的「職位 (Role)」,則我們可以先建立一個 EnsureUserHasRole Middleware,讓該 Middleware 接收一個職位名稱來作為其額外的引數。

額外的 Middleware 引數會被放在 $next 引數之後傳遞給 Middleware:

1<?php
2 
3namespace App\Http\Middleware;
4 
5use Closure;
6use Illuminate\Http\Request;
7use Symfony\Component\HttpFoundation\Response;
8 
9class EnsureUserHasRole
10{
11 /**
12 * Handle an incoming request.
13 *
14 * @param \Closure(\Illuminate\Http\Request): (\Symfony\Component\HttpFoundation\Response) $next
15 */
16 public function handle(Request $request, Closure $next, string $role): Response
17 {
18 if (! $request->user()->hasRole($role)) {
19 // 重新導向...
20 }
21 
22 return $next($request);
23 }
24 
25}
1<?php
2 
3namespace App\Http\Middleware;
4 
5use Closure;
6use Illuminate\Http\Request;
7use Symfony\Component\HttpFoundation\Response;
8 
9class EnsureUserHasRole
10{
11 /**
12 * Handle an incoming request.
13 *
14 * @param \Closure(\Illuminate\Http\Request): (\Symfony\Component\HttpFoundation\Response) $next
15 */
16 public function handle(Request $request, Closure $next, string $role): Response
17 {
18 if (! $request->user()->hasRole($role)) {
19 // 重新導向...
20 }
21 
22 return $next($request);
23 }
24 
25}

也可以在定義 Route 時使用 : 區分出 Middleware 名稱與參數來指定 Middleware 參數。多個參數請使用逗點 (,) 區隔:

1Route::put('/post/{id}', function (string $id) {
2 // ...
3})->middleware('role:editor');
1Route::put('/post/{id}', function (string $id) {
2 // ...
3})->middleware('role:editor');

可終止的 Middleware

有時候,某個 Middleware 可能需要在 HTTP Response 被傳送到瀏覽器後才進行某些動作。若我們在 Middleware 上定義一個 terminate 方法,且網頁伺服器 (Web Server) 使用 FastCGI,則會在 Response 傳送給瀏覽器後會自動呼叫 terminate 方法:

1<?php
2 
3namespace Illuminate\Session\Middleware;
4 
5use Closure;
6use Illuminate\Http\Request;
7use Symfony\Component\HttpFoundation\Response;
8 
9class TerminatingMiddleware
10{
11 /**
12 * Handle an incoming request.
13 *
14 * @param \Closure(\Illuminate\Http\Request): (\Symfony\Component\HttpFoundation\Response) $next
15 */
16 public function handle(Request $request, Closure $next): Response
17 {
18 return $next($request);
19 }
20 
21 /**
22 * Handle tasks after the response has been sent to the browser.
23 */
24 public function terminate(Request $request, Response $response): void
25 {
26 // ...
27 }
28}
1<?php
2 
3namespace Illuminate\Session\Middleware;
4 
5use Closure;
6use Illuminate\Http\Request;
7use Symfony\Component\HttpFoundation\Response;
8 
9class TerminatingMiddleware
10{
11 /**
12 * Handle an incoming request.
13 *
14 * @param \Closure(\Illuminate\Http\Request): (\Symfony\Component\HttpFoundation\Response) $next
15 */
16 public function handle(Request $request, Closure $next): Response
17 {
18 return $next($request);
19 }
20 
21 /**
22 * Handle tasks after the response has been sent to the browser.
23 */
24 public function terminate(Request $request, Response $response): void
25 {
26 // ...
27 }
28}

terminate 方法應接收 Request 與 Response。定義好可終止的 Middleware (Terminable Middleware) 後,請將其加到 Route 列表或 app/Http/Kernel.php 檔案中的全域 Middleware 內。

呼叫 Middleware 上的 terminate 方法時,Laravel 會從 [Service Container] 中解析出這個 Middleware 的新實體。若想讓 handleterminate 都在同一個 Middleware 實體上呼叫的話,請使用 Container 的 singleton 方法來想 Container 註冊這個 Middleware。一般來說,這個註冊應在 AppServiceProviderregister 方法中進行:

1use App\Http\Middleware\TerminatingMiddleware;
2 
3/**
4 * Register any application services.
5 */
6public function register(): void
7{
8 $this->app->singleton(TerminatingMiddleware::class);
9}
1use App\Http\Middleware\TerminatingMiddleware;
2 
3/**
4 * Register any application services.
5 */
6public function register(): void
7{
8 $this->app->singleton(TerminatingMiddleware::class);
9}
翻譯進度
100% 已翻譯
更新時間:
2024年6月30日 上午8:27:00 [世界標準時間]
翻譯人員:
  • cornch
幫我們翻譯此頁

留言

尚無留言

“Laravel” is a Trademark of Taylor Otwell.
The source documentation is released under MIT license. See laravel/docs on GitHub for details.
The translated documentations are released under MIT license. See cornch/laravel-docs-l10n on GitHub for details.