Laravelのミドルウェア
Laravelにおけるミドルウェアは、アプリケーションを表示する前後に追加の処理をするためのもので、言葉からするとオプション的な立ち位置に聞こえますが、認証やメンテナンス時のアクセス回避機能としてのミドルウェアがデフォルトで採用されていることもあり、ブラックボックスのままにはしておけない機能です。
以前に、LaravelでPOSTするためのコードを紹介しましたが、その際にも紹介したXSRF対策でもミドルウェアを用いています。
ミドルウェアの作成
Laravel日本語訳サイトを参考に、ミドルウェアを作成してみたいと思います。ミドルウェアのコード群は通常app¥Http¥Middlewareフォルダにあります。ここに、getかpostのパラメータとして「debug=1」が存在した場合に、デバッグページに移動するGotoDebug.phpというミドルウェアを作って表示させてみます。
まずミドルウェアのひな型を作成して、中身を実装します。ここではデバッグフラグが立っていたら(POSTかGETでdebug=1を渡したら)、デバッグページへリダイレクトされ、それ以外は通常通りのページ表示するような機能を作成します。
> php artisan make:middleware GotoDebug
フォルダに指定した名前のテンプレートが作成されるので、編集します。$requestには文字通りHTTPリクエストが入りここからGETやPOSTされた値を取得することができます。$nextとして受け取るコールバック関数に$requestを渡すと、本来リクエストしたページの処理へ移動します。これは必ずしもreturnと同時に利用する必要はなく、$response=$next($request)としてリクエストページを処理した後で、何かしらの処理を加えることもできます。
GotoDebug.php
<?php
namespace App¥Http¥Middleware;
use Closure;
use Illuminate¥Http¥Request;
class GotoDebug{
public function handle(Request $request, Closure $next){
$param = $request->all();
if (isset($param['debug']) && $param['debug']=='1') {
return redirect('/debug');
}
return $next($request);
}
}
リダイレクト先として設定してあるdebugは別途ビューが作成してあり、ルート上に設定されているものとします。
ミドルウェアの登録
作成したミドルウェアを利用するにはapp¥Http¥Kernal.phpに登録する必要があります。ここにはいくつかのプロパティがあり、どこに設定するかにより挙動が変わってきます。
- $middleware
$middlewareプロパティにミドルウェアを設定すると、すべてのルート上でミドルウェアが機能します。
Kernel.php
... $middleware=[ ¥App¥Http¥Middleware¥TrustProxies::class, ¥App¥Http¥Middleware¥GotoDebug::class, ], ...
urlパラメータとして、?debug=1を加えてアクセスすると/debugに移動するようになります。
- $routeMiddleware
$routeMiddlewareプロパティにミドルウェアを設定すると、ルーターで指定した時のみミドルウェアが機能します。認証用のミドルウェア「auth」はデフォルトではここに設定されています。
あとで名前を指定して呼び出すので、このプロパティは、ミドルウェア名=>ミドルウェアクラスという連想配列となっています。
Kernel.php
... $routeMiddleware=[ 'auth' => ¥App¥Http¥Middleware¥Authenticate::class, 'debug'=>¥App¥Http¥Middleware¥GotoDebug::class, ], ...
ルーターでの記述は、次のようにします。
web.php
... Route::get('/welcome', function () { return view('welcome'); })->middleware(['debug']);
先の設定ではどこのルートにおいても、urlパラメータに?debug=1を設定すれば/debugに移動しましたが、この設定の場合は、/welcomeルート上にいないと(「/welcome?debug=1」としないと)リダイレクトされません。
もし他にも、適用させたいミドルウェアが複数ある場合は、配列に追加することで可能です。またミドルウェアがひとつだけの場合は配列ではなく文字列で指定する事もできます。
- $middlewareGroups
ミドルウェアをグループ化しているプロパティです。デフォルトでは「web」と「api」グループが設定されています。
Kernel.php
... protected $middlewareGroups = [ 'web' => [ ¥App¥Http¥Middleware¥EncryptCookies::class, ¥Illuminate¥Cookie¥Middleware¥AddQueuedCookiesToResponse::class, ¥Illuminate¥Session¥Middleware¥StartSession::class, ¥Illuminate¥View¥Middleware¥ShareErrorsFromSession::class, ¥App¥Http¥Middleware¥VerifyCsrfToken::class, ¥Illuminate¥Routing¥Middleware¥SubstituteBindings::class, ], 'api' => [ // ¥Laravel¥Sanctum¥Http¥Middleware¥EnsureFrontendRequestsAreStateful::class, 'throttle:api', ¥Illuminate¥Routing¥Middleware¥SubstituteBindings::class, ], 'debug-group'=>[ ... ], ...
これらは前述のように$routeMiddlewareとして設定したミドルウェアと同じ使い方ができる事の他に、次のような記述をして内側に記述したRouteそれぞれに、ミドルウェア群を適用させることができます。
web.php
... Route::middleware(['debug-group'])->group(function () { //debug-groupを適用させたいルート Route::get('/page1',function(){...}); Route::get('/page2',function(){...}); Route::post('/page3',function(){...}); });
ちなみにwebとapiのルーティングは、それぞれweb.php、api.phpに書かれています。これらはルーターにはエントリーがなく、RouteServiceProviderで適用されるのですが、プロバイダが実行するコードは次ののようになっています。
web.php
... $this->routes(function () { Route::middleware('api') ->prefix('api') ->group(base_path('routes/api.php')); Route::middleware('web') ->group(base_path('routes/web.php')); }); ...
ミドルウェアグループをそれぞれのコード全体に適用させるようになっています。また、api側ではprefixメソッドを使ってルートにapiというプレフィックスがあることを条件にしています(/api/xxx)。
Throttle
Kernel.phpのapiミドルウェアグループで設定されていた「'throttle:api'」は、他と少し違った書き方なので気になったので調べてみました。
これはThrottleというLaravelの機能で、アクセス回数を制限するものです。ReteLimiterファサードから設定できます。そして、apiと名付けられたthrottleは「RouteServiceProvider」で記述されています。
RouteServiceProvider.php
...
protected function configureRateLimiting(){
RateLimiter::for('api', function (Request $request) {
return Limit::perMinute(60)->by($request->user()?->id ?: $request->ip());
});
}
この関数がbootメソッドから呼び出されていて、ユーザーIDまたは、リモートアドレス毎で1分間に60回以上のアクセスがあると制限をかけるapiという名前のthrottleが生成されます。
Kernel.phpでは、throttle:apiをミドルウェアグループapiに設定しています。そのためapiミドルウェアグループでは、1分間に60回以上のアクセスがあった場合429 TOO MANY REQUESTS エラーを返します。
また、RateLimiter::clearというカウントをクリアするメソッドが用意されていて、先のapiに設定したthrottleはログインが成功するとクリアされるようで、ログインした状態だと何度apiにアクセスしてもエラーにはなりません。
throttleについてはZenn:「Laravel8で429エラーが出たときの対処法」を参考にさせていただきました。
ミドルウェアでセッション変数を利用したい場合
ミドルウェア中にセッション変数の値を取得したり変更したりしたい場合は注意が必要です。デフォルトの設定では、セッションはwebルート上でしか稼働しないようになっています。
また、セッションが始まってからでないと参照や保存ができないので、セッション変数を利用したいミドルウェアはStartSessionミドルウェアの後になるように設定しなければいけません。
これらの設定はApp¥Http¥Kernel.phpに書かれています。
Kernel.php
...
protected $middlewareGroups = [
'web' => [
¥App¥Http¥Middleware¥EncryptCookies::class,
¥Illuminate¥Cookie¥Middleware¥AddQueuedCookiesToResponse::class,
¥Illuminate¥Session¥Middleware¥StartSession::class,
// セッションを使うミドルウェアはこれより下で設定する
¥Illuminate¥View¥Middleware¥ShareErrorsFromSession::class,
¥App¥Http¥Middleware¥VerifyCsrfToken::class,
¥Illuminate¥Routing¥Middleware¥SubstituteBindings::class,
¥App¥Http¥Middleware¥ChangeStore::class,
],
...
筆者が経験したところでいうと$middlewareプロパティに追加したミドルウェアは、そのままだとStartSessionより先に稼働するようです。コード自体はエラーになりませんが、保存や読み込みができませんので意図した挙動にはなりません。
参考にさせていただきましたサイトの皆様、ありがとうございました。