はじめに
タンバリン東京開発チームの鈴木です。よろしくお願いします。
Laravel開発で、jsやcss、画像ファイルをプロジェクト管理ではなく
外部管理(ファイルサーバ等)とした際に、セッションメモリがパンクし、
静的コンテンツが表示されたり、されなかったりしていた。
こちらの原因・解決までを書いていこうと思います。
静的コンテンツ取得のための設計
通常、静的コンテンツをプロジェクト内に配置した場合、
ルーティングの設定は必要なく、HTML等のソース内にファイルパスを入れるだけで参照できるかと思います。
今回は外部管理となることから、
外部ファイルURLをGuzzle等のライブラリからGETで呼び出す
ファイル情報を取得
フロントに情報を返す
上記手順を踏む必要があり、外部ファイル参照用のルーティング設定が必要でした。
ルーティング設定
外部ファイル参照用のルーティングとして以下を「./routes/web.php」に設定。
【./routes/web.php】 Route::get('{otherPages}', 'StaticFileController@index') ->where(['otherPages' => '.*']);
URL構成として「./hoge」の他に「./hoge/fuga」や「./hoge/fuga/piyo」等の複数ディレクトリ構成にも対応するため、 where句でワイルドカードを指定しています。
上記ルーティングを追加することで、指定されていないURLにアクセスした場合に「StaticFileController」が呼ばれるようになります。
StaticFileController内で、Guzzle等のライブラリを利用し、外部URLに対してGET呼び出し→情報取得を行いますが、今回の原因とは無関係のため省略させていただきます。
本事象発覚
動作環境に上げてから事象が発覚。 外部から取得している画像で404エラーが発生しました。必ず、エラーとなるわけではなく取得できる場合と、出来ない場合が発生しました。
404エラーとなる画像パスに、直アクセスした場合は正常に表示されるため 事象発覚時点では、Laravel内のソースに問題はないと過信しキャッシュサーバーとして利用しているRedisの設定等を疑っていました。
原因調査
Redisを確認したところ以下エラーが出ていることが判明。 内容としては、クライアント数が最大に達したとのエラー。
Exception 'Predis\Connection\ConnectionException' with message '
AUTH
failed: ERR max number of clients reached []' in /app/vendor/predis/predis/src/Connection/AbstractConnection.php:155
テスト環境ということもあり、Redisがパンクするほどのアクセスは発生しておらずこちらを疑いました。
Laravelはルーティングを経由したアクセスをした際、CSRFトークンが自動で発行するよう標準実装されています。
「./app/Providers/RouteServiceProvider.php」内で「./routes/web.php」に記述したルーティングに対して適応するミドルウェアが指定されています。
【./app/Providers/RouteServiceProvider.php】 Route::middleware('web') ->namespace($this->namespace) ->group(base_path('routes/web.php'));
「./app/Http/Kernel.php」内、webグループのミドルウェアが各ルーティングに自動で適応されています。
【./app/Http/Kernel.php】 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:60,1', \Illuminate\Routing\Middleware\SubstituteBindings::class, ], ];
発行されたCSRFトークンはRedisに保存していました。
通常、コントローラーリクエストの1CSRFトークンのみ発行・保存されます。
静的コンテンツを外部管理としたことで、例えば1ページに100画像あった場合、
「コントローラーリクエスト + 100画像リクエスト = 101 CSRFトークン」がアクセスされるたびに発行されRedisに保存されていました。
実際にRedisの中身を確認すると、URLに入るのは、アクセスしたページのURLのはずですが、画像パスに対してCSRFトークンが保存されていました。
【Redis内容】 a:4:{ s:6:"_token"; s:40:"xxxxxxxxxx"; b:1; s:9:"_previous"; a:1:{ s:3:"url"; s:55:"xxxxx.png"; } s:6:"_flash"; a:2:{ s:3:"old"; a:0:{} s:3:"new"; a:0:{} } }
アクセスする度に、無駄なCSRFトークンがRedisに保存されたことで
RedisがパンクしCSRFトークンの保存ができないことでエラーが発生。
解決
静的コンテンツ参照用のルーティングでCSRFトークンが発行されることが原因ということで、 トークンが発行されないようルーティングの修正を行いました。
簡単な修正として、前述した「./app/Http/Kernel.php」内webグループから、対象のミドルウェアをコメントアウトすることで対応は可能でした。
しかし、上記対応を取った場合、通常のルーティングでもCSRFトークンが発行されなくなるためセキュリティリスクがあり対応として見送り。
調べたところLaravelには、「withoutMiddleware」という便利な機能があるではありませんか!
withoutMiddlewareとは?
指定したミドルウェアを適用しないようする標準機能
こちらの機能を静的コンテンツ参照用のルーティングに対して追加。
【./routes/web.php】 Route::get('{otherPages}', 'StaticFileController@index') ->where(['otherPages' => '.*']) ->withoutMiddleware([ \Illuminate\Session\Middleware\StartSession::class, \Illuminate\View\Middleware\ShareErrorsFromSession::class, ]);
セッション発行自体が不要なため、セッションの開始から除外とすることで無駄なCSRFトークンの発行がなくなり本件解決となりました…!
まとめ
自分自身のコードの過信は良くないと改めて。 また、標準機能(ミドルウェア)の動きを理解することの大切さを学ぶことが出来ました。
静的コンテンツを外部で管理するレアケースではありますが、参考になればと思います。 以上です!