九保すこひ@フリーランスエンジニア|累計300万PVのブログ運営中
相変わらず大好きな Laravel でウェブサイトの開発を続けているのですが、タイトルのように今回どうにもよくわからない状態に陥ってしまったので解決までの手順を記事にしたいと思います。
【追記(2017.06.11)】
とてもシンプルな解決法を後で思いつきました。
それは、Controller の中で HTTPメソッドを無理やり変更してやる事です。
storeCurrentUrl() では GET の場合のみの前ページのURLをセッションに格納するようにしているのでこれで対応できそうです。
$request->setMethod('POST');
【環境は?】
Larvel 5.4 & PHP 7
【不具合の詳細は?】
1.フォームから通常の submit をする。
2.FormRequest 内に作っている rule (バリデーション)に引っかかりリダイレクト
3.元の場所に戻る、、、、と思いきやなぜか画像のURLへリダイレクトされる。
【現象が起こるのは?】
ブラウザは Chrome でした。
私は(未だに?)開発には FireFox を使っているのですが、こちらでは問題なくリダイレクトされ不具合は発生しません。
謎は深まるばかりです。
【調査開始】
とにかくこれは何とかしなければということで調査を開始。
まず現在開発している管理サイトは vue を結構多めに使っているので onSubmit の段階で何かおかしなリダイレクトをしているのではないかと思いチェックをしてみました。
が、結果としては JavaScript がリダイレクトしているわけではなくきちんと submit されていました。
んー、ではやっぱり Laravel 側に問題があるのか。
と思ったので今度は Controller のメソッド内で使っている Request (つまりバリデーション)を疑ってみました。
しかしデータもきちんと送信されてきているし何も変わったことがありませんでした。
もう通常のアプローチでは解決ができないと思い、この時点で Laravel 本体のコード側にもアプローチをすることを決意!(笑)
バリデーションを書いている Request の拡張元である FormRequest をチェックするとどうやらバリデーション失敗時には
getRedirectUrl() というメソッドからリダイレクトURLを取得しているようでした。
そこで、このリダイレクトURL をdie() でチェックしてみると、なんとリダイレクトURLがしっかりと問題の画像URLになっていました。
なぜ、、、
では、今度はこのメソッドの調査だと思い親メソッドを遡って行きました。すると
1.同じく FormRequest の getRedirectUrl()
2.Illuminate\Routing\UrlGenerator の getPreviousUrlFromSession()
3.Illuminate\Session\Store の getPreviousUrlFromSession()
という場所にまで行きつきました。
んー、やはりというか当然というかセッションに格納された「直前に表示したページ」がURLの元になっています。
ということはあるどこかのタイミングで URL が画像のものに上書きされてしまっているということか、、、、でも、一体どこ!?
【犯人を発見!】
このまま迷宮入りするの、、、?
と考えていた頃、ある一つの事実が発生しました。
それは、
違うユーザーでログインした場合はリダイレクトがうまく行く!という事実でした。
ん????
さっきのユーザーにあって今のユーザーにないもの、、、、?
分かった!!!!!
ユーザーのプロフィール画像だ!
でも、なんで、、、
あ!このプロフィール画像は外部公開してはいけないので Laravelプログラムを通して storage から画像データを読み込み、 Content-Type を image/jpeg として表示している!
ということは画像が読み込まれた時点で「直前に表示したページ」がセッションに格納(つまり正しいURLが上書き)されてしまう!
なるほど。
つまりこの不具合の手順は以下。
1.フォーム入力ページを表示(ここで最初の直前ページが格納される)
2.JS や CSS などと一緒にプロフィール画像も読み込まれる(←ここでさっきの正しいURLが上書きされる)
3.バリデーションが失敗するとリダイレクト失敗。。
こんなところに犯人がいたとは、、、
【解決するには、、、?】
では、この不具合を解決するにはどうすればいいのでしょうか。
少しインターネット上で調べてみると、
Illuminate\Session\Middleware\StartSession
を拡張して storeCurrentUrl() を以下のように画像が入ってきた場合は直前ページのURLの格納はしないようにする必要があるとのことでした。
protected function storeCurrentUrl(Request $request, $session) { if ($request->method() === 'GET' && $request->route() && ! $request->ajax() && !preg_match('/(\.jpg|\.jpeg|\.png|\.gif)/', $request->url())) { $session->setPreviousUrl($request->fullUrl()); } }
そしてこの拡張した StartSession を以下のように App\Http\Kernel にセット(通常のものはコメントアウト)。
protected $middlewareGroups = [ 'web' => [ \App\Http\Middleware\EncryptCookies::class, \Illuminate\Cookie\Middleware\AddQueuedCookiesToResponse::class, // \Illuminate\Session\Middleware\StartSession::class, \App\Http\Middleware\StartSessionEx::class, // \Illuminate\Session\Middleware\AuthenticateSession::class, \Illuminate\View\Middleware\ShareErrorsFromSession::class, \App\Http\Middleware\VerifyCsrfToken::class, \Illuminate\Routing\Middleware\SubstituteBindings::class, \App\Http\Middleware\HttpsProtocol::class, ], 'api' => [ 'throttle:60,1', 'bindings', ], ];
よし!これでやっと今回の騒動も終了だぞ!
と思ったらまだ終わっていませんでした、、、、ログインができない。。。
というか、 TokenMismatchException が出てしまう。これは csrf 対策のためのものなのは知ってましたが、なんでまたこんな時に、、、?
そこで、元々の SessionStart を呼び出している部分を検索。(←こういうとき phpstorm は強力なツールだと実感します)
すると、Illuminate\Session\SessionServiceProvider というファイルを発見。つまり ServiceProvider をきちんと整備してあげないと csrf トークンが拡張したものと違ってくるので Exception が発生しているようです。
ということで仕方なく、またこのクラスを拡張したものを以下のように作成しapp.php で元の SessionServiceProvider と入れ替えることにしました。
(app/Providers/SessionServiceProviderEx.php)
<?php namespace App\Providers; use App\Http\Middleware\StartSessionEx; use Illuminate\Session\SessionServiceProvider; class SessionServiceProviderEx extends SessionServiceProvider { /** * Register the service provider. * * @return void */ public function register() { $this->registerSessionManager(); $this->registerSessionDriver(); $this->app->singleton(StartSessionEx::class); } }
(config/app.php)
Illuminate\Auth\Passwords\PasswordResetServiceProvider::class, // Illuminate\Session\SessionServiceProvider::class,
これを実行するとさっきまで腹立たしく表示されていた TokenMismatchException の消え、ログインもバリデーションのリダイレクトも問題なく動くようになりました!
にしても、これって何かもっと簡単に対策することはできないのでしょうか???S
というのも、画像を特定のメンバー以外には見せたくないという状況は通常よく考えられますし、その場合は storage の public 化も使えません。
もちろん Request 内で $redirect を使うのもひとつの方法なのでしょうが、いちいち全ての Request に書き込むのもめんどうですし、なにより insert 時 と update 時で Request を共有しているので場合分けのコードも書かなきゃいけない。
なんとか Controller 内、もしくは route のグループ内などで直前ページへの格納を阻止できる設定ができればなー、と考えてました。
もし何かいい方法があればぜひ教えてください。
ではではー。m_ _m