requestのヘッダにはcookieが入っているのに、Laravelからcookieを取得しようとすると取得できないと言う現象にハマってしまいました…
$_COOKIE['EXAMPLE']
で該当のcookieを取得できるのに、
$request->cookie('EXAMPLE')
だと取得できない…
調査して解決したのでメモしときます。
TL;DR
Laravelでcookieを扱うミドルウェアを追加する場合は、EncryptCookies
ミドルウェアの後に定義しましょう。
環境
名前 | バージョン |
---|---|
PHP | 7.1.6 |
Laravel | 5.5.14 |
現象
どのURLでもcookieが設定されていなければ付与し、設定されていればそのままにする、という処理を実装する必要がありました。
全てのURLが対象となるのでMiddlewareで実装することにしました。
以下のMiddlewareです。
リクエストヘッダにcookieがあるかチェックし、なければレスポンスにcookieを付与します。
<?php
namespace App\Http\Middleware;
use Closure;
class ExampleMiddleware
{
/**
* Handle an incoming request.
*
* @param \Illuminate\Http\Request $request
* @param \Closure $next
* @return mixed
*/
public function handle($request, Closure $next)
{
$response = $next($request);
if (!$request->cookie('EXAMPLE')) {
$response->cookie('EXAMPLE', rand(), 60 * 24); // 1日
}
return $response;
}
}
Kernel.phpを修正して作成したミドルウェアを読み込ませるようにします。
作成したMiddlewareをKernel.phpに登録します。
<?php
namespace App\Http;
use Illuminate\Foundation\Http\Kernel as HttpKernel;
class Kernel extends HttpKernel
{
/**
* The application's global HTTP middleware stack.
*
* These middleware are run during every request to your application.
*
* @var array
*/
protected $middleware = [
\Illuminate\Foundation\Http\Middleware\CheckForMaintenanceMode::class,
\Illuminate\Foundation\Http\Middleware\ValidatePostSize::class,
\App\Http\Middleware\TrimStrings::class,
\Illuminate\Foundation\Http\Middleware\ConvertEmptyStringsToNull::class,
\App\Http\Middleware\UserTrackCookie::class, // ★追加
];
/**
* The application's route middleware groups.
*
* @var array
*/
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',
'bindings',
],
];
/**
* The application's route middleware.
*
* These middleware may be assigned to groups or used individually.
*
* @var array
*/
protected $routeMiddleware = [
'auth' => \Illuminate\Auth\Middleware\Authenticate::class,
'auth.basic' => \Illuminate\Auth\Middleware\AuthenticateWithBasicAuth::class,
'bindings' => \Illuminate\Routing\Middleware\SubstituteBindings::class,
'can' => \Illuminate\Auth\Middleware\Authorize::class,
'guest' => \App\Http\Middleware\RedirectIfAuthenticated::class,
'throttle' => \Illuminate\Routing\Middleware\ThrottleRequests::class,
];
}
これで動作するかと思いましたが期待した動作になりません。
cookieがあれば同じ値を使用し続けてほしいのですが、ExampleMiddlewareで毎回rand()で新しい値がセットされてしまいます。
$_COOKIE['EXAMPLE']
でvar_dumpすると値が入っていますが、$request->cookie('EXAMPLE')
をvar_dumpしても値が入っていません。
やったこと
Laravelのマニュアル を読むと
Laravelフレームワークが作成するクッキーは全て暗号化され、認証コードで著名されています。つまりクライアントにより変更されると、無効なクッキーとして取り扱います。
とありました。
もしかして無効にされているんじゃないか?と思って、EncryptCookiesミドルウェアの$except
にcookie名を追加して暗号化対象外にしました。
そうすると、 $request->cookie('EXAMPLE')
で値が取得できるようになりました。
変更されると無効になるということは、登録したMiddlewareの順序によっては値が変わってしまって取得できないこともあるのでは?と検討をつけました。
EncryptCookiesミドルウェアはLaravelプロジェクトを作成すると自動で生成されるファイルです。
実態はIlluminate\Cookie\Middleware\EncryptCookies
にあります。
このクラスのhandleメソッドを見ると
/**
* Handle an incoming request.
*
* @param \Illuminate\Http\Request $request
* @param \Closure $next
* @return mixed
*/
public function handle($request, Closure $next)
{
return $this->encrypt($next($this->decrypt($request)));
}
とあり、リクエストを復号化したのち次のミドルウェアに処理を委譲しその戻り値を暗号化しています。
ミドルウェアの実行順序ですが、
-
$middleware
に登録された順でリクエストが処理される -
$middlewareGroups
に登録された順でリクエストが処理される -
$routeMiddleware
のうちのミドルウェアの利用を宣言した順でリクエストが処理される -
$routeMiddleware
のうちのミドルウェアの利用を宣言した逆順でレスポンスが処理される -
$middlewareGroups
に登録された逆順でレスポンスが処理される -
$middleware
に登録された逆順でレスポンスが処理される
なので、失敗したケースでは
1ExampleMiddleware -> 2EncryptCookies(request復号化) -> 3EncryptCookies(response暗号化) -> 4ExampleMiddleware
の順で処理されています。
Laravelフレームワークが作成するクッキーは全て暗号化され、認証コードで著名されています。つまりクライアントにより変更されると、無効なクッキーとして取り扱います。
復号化された値と暗号化された値で比較するために、値が変わって無効扱いにされてしまって取得できないものと思われます。
順序を以下に入れ替えます。
1EncryptCookies -> 2ExampleMieddleware -> 3ExampleMieddleware -> 4EncryptCookies
この順序だと暗号化対象外の$except
に入れなくても$request->cookie('EXAMPLE')
で値を取得することができました!
復号化された値と復号化された値で比較するため、同じものと認識されるので取得できるものと思われます。
$middleware
での登録をやめ、$middlewareGroups
で登録するよう変更しました。
protected $middleware = [
\Illuminate\Foundation\Http\Middleware\CheckForMaintenanceMode::class,
\Illuminate\Foundation\Http\Middleware\ValidatePostSize::class,
\App\Http\Middleware\TrimStrings::class,
\Illuminate\Foundation\Http\Middleware\ConvertEmptyStringsToNull::class,
];
protected $middlewareGroups = [
'web' => [
\App\Http\Middleware\EncryptCookies::class,
\App\Http\Middleware\UserTrackCookie::class, // ★必ずEncryptCookiesより順序は後ろにすること
\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',
'bindings',
],
];
Laravelのミドルウェアでcookieを読み込んだりセットする場合はEncriptCookies
ミドルウェアより前に定義してしまうと、cookieが取得できない場合があるので注意!
Laravelでcookieを扱うミドルウェアを追加する場合は、EncryptCookies
より前に定義せず、後に定義しましょう。