これでセッションとクッキーの理解はスッキリ!(Laravel編)
Session(セッション)やCookie(クッキー)という単語は嫌というほど聞いているにも関わらず認証やフォームでLaravelがうまく制御してくれているので詳細を理解せずに利用している人またはSessionやCookieの仕組みはなんとなく理解できているけど実際にどのような情報をやり取りしているのかわからないという人を対象に”Laravel”でのSessionとCookieについて説明を行っています。
Sessionはサーバ(Laravel)側、Cookieはクライアント(ブラウザ)側で管理を行いデフォルトではサーバ側のSession情報はファイルの中に保存されます。ファイル以外にもSessionを保存する方法は複数あるので今回はテーブルを使って確認していきます。テーブルを利用するのはSession情報の管理は1つの方法でなく複数存在し、他の管理方法を知ることでSessionの理解を深めることを目的にしています。
本文書を読み終えるとSession、Cookieにはどのような情報が保存されているのか。middlewareがSessionとCookieに与える影響を理解することができます。
目次
Session、Cookieとは
HTTPはステートレスなためユーザがあるページを閲覧して次のページに移動すると同一のユーザが引き続きページを閲覧しているかどうかサーバ側ではわかりません。そのような状態ではシステムにログインしたとしてもログイン状態が管理できないので会員システムのようなアプリケーションやECサイトのように商品をショッピングカードに保存することはできません。
この問題を解決するために存在するのがSessionとCookieです。クライアントであるブラウザとサーバ(ここではLaravel)間でSession ID(ブラウザではCookie、サーバではセッション情報に保存)を共有し、そのSession IDを照会することでユーザを識別することが可能になります。またECサイトであればSessionの中にショッピングカードの情報を保存することでページを移動しても情報を保持することができます。
ブラウザ側でSession IDを保存する場所がCookieとなり、サーバ側でSession IDを保持する場所がSessionになります。
上記で説明したことを実際にLaravelを利用し確認することでSessionとCookieがどのような役目を果たしているかを理解を深めることができます。
CookieはSessionだけではなくアクセス解析にも利用することができます。JavaScriptからもCookieにアクセスすることができるので JavaScriptを使ってアクセスのあったブラウザ情報やアクセス頻度を記録し外部の分析用サーバに送信するといったことが可能です。
環境構築
sessionsテーブルの作成を行うため事前にデータベース、usersテーブルの作成、ログイン認証の機能が使える状態にしておく必要があります。本文書では環境設定については割愛しています。
Sessionsテーブルの作成
Laravel6まではLaravelの公式マニュアルにあるsessionsテーブルのスキーマ(=テーブルの構成)を参考に作成していましたがLarave7からはsessionsテーブルのマイグレーションファイルをphp artisanコマンドを利用して作成することが可能になりました。
Laravel7以降
php artisan session:tableコマンドを実行します。database¥migrationsディレクトリの下にマイグレーションファイルが作成されます。
% php artisan session:table
Migration created successfully!
Laravel6の場合
php artisan make:migrationコマンドでsessionsテーブル用のマイグレーションファイルを作成します。
$ php artisan make:migration create_sessions_table
database¥migrationsディレクトリの下にマイグレーションファイルが作成されるので、ファイルに以下の列情報を記述します。
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
class CreateSessionsTable extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::create('sessions', function ($table) {
$table->string('id')->unique();
$table->unsignedInteger('user_id')->nullable();
$table->string('ip_address', 45)->nullable();
$table->text('user_agent')->nullable();
$table->text('payload');
$table->integer('last_activity');
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::dropIfExists('sessions');
}
}
sessionsテーブルは、id, user_id, ip_address, user_agent, payload, last_activityの列で構成されています。それぞれの列にどのような情報が入るかは動作確認を行う中で理解することができます。
Sessionsテーブルの作成
マイグレーションファイルを作成したらphp artisan migrateコマンドを実行してテーブルの作成を行ってください。
% php artisan migrate
Migrating: 2021_03_07_003023_create_sessions_table
Migrated: 2021_03_07_003023_create_sessions_table (6.96ms)
Session Driverの変更
先ほどサーバ側ではSession IDをSessionに保存すると説明しましたがサーバのどこに保存するかをSession Driverを使って設定します。保存できる場所にはデフォルトのファイルの他にデータベース、Cookie、redisなどさまざまな場所がありますが今回はデータベースを利用します。変更は.envファイルで行うことができます。
.envファイルのSESSION_DRIVERを下記のようにfileからdatabaseに変更します。
SESSION_DRIVER=database
.envファイルでSESSION_DRIVERの変更を行いましたがSessionに関する情報はconfigのsession.phpファイルで行います。SESSION_DRIVERの設定値もこのファイルで利用されます。
セッションとクッキー
セッションとクッキーを目で確認
LaravelのSessionとCookieにはどのような情報が記述されるのか実際に目でみてみましょう。
ブラウザを起動して、”/”にアクセスします。アクセスする際はLaravelへのログインを行っていない状態でアクセスしてください。
ブラウザ側でCookieの中身を確認します。
ChromeのExtensionで確認
使ってみると説明を行うためのCookie情報が読みやすかったのでCookie ManagerというChomeのExtentionを利用しています。次に説明するChromeのデベロッパーツールからでも確認できるのでCookie Managerが使えない場合や使いたくない場合はChromeの機能を利用して確認してください。
アクセスしたときに作成されるLaravelに関連するCookieは以下の通り2つです。Name列にはXSRF-TOKENとlaravel_sessionの2つを確認することができます。Value列には文字列が入り、Expiration列には時刻が表示されています。
デベロッパーツールで確認
ChromeのデベロッパーツールのApplicationタブからもCookieの中身は確認できます。
Cookieの名前
デフォルトではcookieの名前がlaravel_sessionとなっていますがこれは固定の名前ではなくsession.phpファイルのcookieのパラメータで変更可能です。.envファイルのAPP_NAMEの名前を変更してもcookieの名前は変わります。
'cookie' => env(
'SESSION_COOKIE',
Str::slug(env('APP_NAME', 'laravel'), '_').'_session'
),
テーブルのSessionを確認
サーバ側でSession情報を保存するsessionsテーブルにはどのような情報が保存されているのかも確認してみましょう。
データベース管理GUIにはTablePlusを利用しています。
テーブルにはマイグレーションファイルで設定されているid, user_id, ip_address, user_agent, payload,last_actitity列が確認できます。user_id以外はすべて埋まっていることが確認できます。ip_addressにはアクセスしたIPアドレス、user_agentにはアクセスしたブラウザの情報が入っています。user_id, payload, last_activityについてはこれから説明を行っていきます。
SessionとCookieで同じIDを保持すると説明しましたが、sessionsテーブルのidに対応する値を持つものはCookie側にはありません。
その理由はCookieが持つIDが暗号化されているためです。Cookieの暗号化はLaravel middlewareで行われています。
middleware(ミドルウェア)とは
middleware(ミドルウェア)はブラウザからLaravelにアクセスした際に実行される処理群です。middlwareのイメージとして表すと下記のようにオニオンの層のようになっており、1つ1つの層が独立した役割を持ちブラウザから送信されてくるHTTPリクエストに対してチェックを行います。最もわかりやすい例はアクセスしてくるユーザのリクエストが認証済かどうかSession, Cookieを使ってチェックします。チェックにパスすればそのまま処理を継続することができますがチェックをパスしなければログイン画面にリダイレクトされます。
middlewareの中身
各層がどのような処理を行うのかはapp¥Http¥Kernel.phpファイルの中に記述されています。3つの変数($middleware, $middlewareGroups, $routeMiddleware)で定義されています。アクセス元がブラウザであろうとAPIであろうと必ず実行されるのが最初に記述されている$middlewareでグローバルミドルウェアと呼ばれます。
protected $middleware = [
// \App\Http\Middleware\TrustHosts::class,
\App\Http\Middleware\TrustProxies::class,
\Fruitcake\Cors\HandleCors::class,
\App\Http\Middleware\PreventRequestsDuringMaintenance::class,
\Illuminate\Foundation\Http\Middleware\ValidatePostSize::class,
\App\Http\Middleware\TrimStrings::class,
\Illuminate\Foundation\Http\Middleware\ConvertEmptyStringsToNull::class,
];
配列に入ったClass1つ1つがmiddlewareで各クラスファイルがRequestに対して独立処理を行い、処理が終わると次のmiddlewareにRequestを渡します。Requestには$middlewareに登録された処理がすべて実行されます。各middlewareは独立しているため新たに追加することも削除することも可能です。追加したい場合は$middlewareの配列にClassを追加、削除したい場合は$middlewareの配列からClassを削除します。middlewareの作成はphp artisan make:middlewareコマンドで行うことができます。
$middlewareGroupsはweb, apiに別れてグループ化されています。どちらも$routeMiddlewareに登録されているmiddlewareのauth(認証)に関連しておりauthを利用した場合はweb, apiのどちらからを利用することになります。web, apiはそれぞれweb guard, api guardとも呼ばれweb guardはブラウザからのアクセスに対しての認証処理を行うために利用します。api guardはLaravelが付与したTokenを利用したアクセスに対して認証処理を行うために利用します。SessionやCookieに関する処理はwebのmiddlewareに含まれています。
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:api',
\Illuminate\Routing\Middleware\SubstituteBindings::class,
],
];
最後の$routeMiddlewareはグローバルミドルウェアの$middlewareのようにすべてのRequestに対して実行されるmiddlewareではなくルーティングに対して個別に適用することができるmiddlewareです。下記の例では$routeMiddlewareのキーであるauthを指定し、Tokenに対する認証処理を行うためapiを設定しています。Tokenを使ってLaravelにアクセスがあるとauthとapiのmiddlewareを組み合わせて認証処理が行われます。
Route::middleware('auth:api')->
get('/user', function (Request $request) {
return $request->user();
});
ブラウザでLaravelにアクセスするとcookieが自動で作成されていることは先ほど確認できました。ではそれらのcookieはいつ作成されるのでしょう?
cookieの作成はLaravelから戻されるResponseヘッダーの中の”Set-Cookie”の値をブラウザが受け取ることで作成されます。
LaravelのSession IDを保存するCookie(デフォルト名はlaravel_session)の作成処理はmiddlewareのStartSessionの中で行われています。作成の詳細を確認したい場合は\Illuminate\Session\Middleware\StartSessionのaddCookieToResponseメソッドの中をみてください。
cookieの暗号化を解除することでcookieとsessionで同じSession IDを持つことが確認できます。
middlewareに設定されている処理はclass毎に独立しているため追加、削除(利用するか利用しないか)が可能です。cookieの暗号化の処理を確認するために暗号化の機能を外してみましょう。
app¥http¥kernel.phpファイルを開いてEncryptCookiesをコメントアウトしてください。
'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,
],
コメントアウトが完了したら、再度ブラウザでアクセスしてください。Cookieのどちらの値もEncrpytCookiesをコメントアウトする前より明らかに短くなっていることが確認できます。
sessionsテーブルを確認してみましょう。sessionsテーブルの2行目のid(BiaGPbarYRd9TwHFdfo70omcCUdLuCddN5vN9TEF)がCookieの中に入っているlaravel_session値と同じになっていることが確認できます。
この結果からcookieとsessionで同じIDが保持できていることが確認できました。
last_activityとExpiration
ユーザを作成するためにRegisterリンクをクリックしてユーザ登録画面に移動します。
Session情報のlast_actitity列を確認してみてください。cookieの暗号化を確認した時よりも数字が増えていることが確認できます。少しおいて画面のブラウザをリロードしてみてください。さらに数字が増えていることが確認できます。
この数字はunixタイムスタンプと言われる数字で変換を行うと実行した日付に戻すことができます。last_activityは最後のアクティビティという意味を持つのでページにアクセスする毎に更新されることがわかります。
Cookie側にも時間に関するExpiration(有効期限)を持っています。この値もページにアクセスする毎に増えていることが確認できます。
しかしCookieのExpirationは現在の時刻ではなく現在の時刻の2時間後になっています(各自の環境でアクセス時間+2時間になっていることを確認)。なぜ、有効期限が2時間後なのでしょうか。これはデフォルトで有効期限が2時間に設定されているためです。
Sessionに関する設定はすべてconfig/Session.phpファイル内に記述されており、有効期限はlifetimeパラメータで設定を行うことができるので確認してみましょう。デフォルトでは120に設定がされています。単位は分です。
'lifetime' => env('SESSION_LIFETIME', 120),
.envファイルのSESSION_LIFETIMEパラメータで変更することが可能です。2時間から4時間に変更してみましょう。120から240に変更してください。
現時刻が16時すぎのためアクセスを行うと18時の2時間後から20時の4時間後に変更になっていることが確認できます。
Sessionのlast_activityとCookieのexpirationの確認でそれらの時刻がなにを意味するのかとアクセス毎に常にそれらの情報が更新されていることがわかりました。
ユーザのログインによる変化
ユーザがログインしたらどのようにSessionの情報が変わるのか確認してみましょう。ユーザ登録を行い、そのユーザでログインを行ってください。
ユーザがログインした瞬間にcookieのlaravel_sessionの値が更新されることを確認してください。
また、sessionsテーブルを見るとCookieと同じIDを持っている行のuser_idに値が入っているのがわかります。ログインする前はuser_idはNULLでした。このIDはログインを行ったユーザのIDです。別のユーザでログインすると別のユーザのIDがuser_idに入ります。
ログインを行うことでSessionのuser_idがログインしたユーザのIDに設定されるということがわかりました。このIDによりどのユーザの認証が完了しているのかをSession情報から把握することができます。
Sessionsテーブルのpayload
sessionsテーブルのpayload以外の列の情報についてはここまでの動作確認で理解することができました。次はpayloadの内容を確認していきます。
payloadもidと同様に長い文字列なので暗号化されているかと思うかもしれませんが、暗号化ではなくbase64でエンコードが行われています。base64でエンコードされる前のデータが何なのかを確認していきます。
Sessionの処理はmiddelwareのStartSession.phpを使って行われているのでその処理をコードを追っていきます。StartSession.phpはkernel.phpファイルで確認できます。
protected $middlewareGroups = [
'web' => [
\App\Http\Middleware\EncryptCookies::class,
\Illuminate\Cookie\Middleware\AddQueuedCookiesToResponse::class,
\Illuminate\Session\Middleware\StartSession::class, //ここ
SessionStart.phpのコードを確認していくとSessionの最後でsaveSessionメソッドが実行されます。saveSessionメソッドはどのDriverを利用するかによってSessionの保存処理は異なりますがここではdatabase driverを利用しているのでDatabaseSessionHandler.phpのwriteメソッドでSessionsテーブルへの書き込みが行われます。そのwriteメソッドで渡される$dataがbase64でencodeされています。
public function write($sessionId, $data)
{
$payload = $this->getDefaultPayload($data);
//略
}
protected function getDefaultPayload($data)
{
$payload = [
'payload' => base64_encode($data),
'last_activity' => $this->currentTime(),
];
$dataがどのような値を保持しているのかを確認するためにDatabaseSessionHandler.phpを呼び出しwriteメソッドを実行しているIlluminate¥Session¥Store.phpを確認します。Store.phpのsaveメソッドの中で$dataは$this->attribuetesだということがわかるので$this->attributesの中身を確認します。
public function save()
{
$this->ageFlashData();
dd($this->attributes);
$this->handler->write($this->getId(), $this->prepareForStorage(
serialize($this->attributes)
));
$this->started = false;
}
ddで$this->attribuetesを確認するとtokenなどの値が保存されていることがわかります。また入力フォームで入力した値を保持する_flashのold, newもpayloadに保存されていることがわかります。
_flashの値がどのように変化するのか意図的にログインユーザに登録されていないユーザでログインを行って値を確認したのが下記です。
_flashのoldにも値が入り、errorsも追加されています。errorにはThese credentials do not match our recodesも確認できます。バリデーションエラーもSessionの中に保存されることがわかります。
Sessionに値を入れる
次にSessionに値を入れた場合にpayloadがどのように変化するのか確認してみましょう。
web.phpファイルを開いてsessionヘルパー関数を使ってSessionにtestを追加します。設定後、ブラウザでアクセスしてSessionにtestを登録しても表示される画面には何も影響はありません。
Route::get('/', function () {
session()->put('test','セッションにデータ追加');
return view('welcome');
});
Sessionにtestを追加した後に先ほどと同様の場所で$this->attribuetesの中身を確認します。
public function save()
{
$this->ageFlashData();
dd($this->attributes);
$this->handler->write($this->getId(), $this->prepareForStorage(
serialize($this->attributes)
));
$this->started = false;
}
新たにtestが追加されていることが確認できます。このように英数字のただの羅列だと思われていたsessionsテーブルのpayload列はSessionに保存されている値だということがはっきりわかりました。Sessionになにか値を設定するとこのpayloadに保存されることになります。
CookieのXSRF_TOKENとは
ブラウザ側に保存されているCookieの中には、Session IDを保存したLaravel_sessionとXSRF_TOKENの2つがありました。Session IDについてはこれまでの説明で理解することができましたが、XSRF_TOKENは一体なんなのでしょう。
XSRF_TOKENはCSRF(クロスサイトリクエストフォージェリ)対策に使用するためのTOKENデータです。通常のPOSTアクセスではフォームに埋め込んだCSRFトークンを利用して、その値をチェックすることでフォームを送ってきたページが正しいページなのかを判断します。XSRF_TOKENは入力フォームではなくaxiosを利用してpostリクエストを送信する際に利用します。
XSRT_TOKENとCSRF_TOKENがありますが、両者の違いは暗号化を行っているかどうかです。XSRF_TOKENは暗号化されているためLaravel側ではmiddlewareのVerifyCsrfTokenに復号化を行う処理が入っています。
Laravelのデフォルトでaxiosを利用することが可能です。デフォルトの設定のままLaravelを利用すればXSRF_TOKENを自動でheaderに組み込んでPOSTリクエストを送信してくれます。XSRF_TOKENとaxiosのおかげでaxiosを利用する場合はcsrfの設定する必要はありません。ajaxを利用する場合はmetaタグでcsrfトークンを生成してその値をHeaderに追加してLaravelに送信する必要があります。
<meta name="csrf-token" content="{{ csrf_token() }}">
//略
$.ajaxSetup({
headers: {
'X-CSRF-TOKEN': $('meta[name="csrf-token"]').attr('content')
}
});
VerifiCsrfTokenのmiddlewareをコメントアウトするとCookieにはXSRF_TOKENは保存されなくなります。それはVerifyCsrfTokenの中のaddCookieToResponseメソッドでヘッダーの”Set-Cookie”にXSRF-TOKENが設定されてブラウザに戻される処理が行われているためです。
ファイルに保存されたsession情報
SESSION DRIVERをdatabaseからfileに変更し、fileにはどのようなSession情報が入っているか確認しておきましょう。ファイルの保存場所はstorage/framework/sessionsディレクトリにSession毎に1つのファイルとして保存されます。ファイル名はSession IDになっています。
中身を確認すると下記のように記述されています。base64ではエンコードされていません。
a:3:{s:6:"_token";s:40:"Zl5eRTQtr431ZKIA8XuoGk1FVFOQQVRlzKBFeYzQ";s:9:"_previous";a:1:{s:3:"url";s:21:"http://localhost:8000";}s:6:"_flash";a:2:{s:3:"old";a:0:{}s:3:"new";a:0:{}}}
Session情報の表示
Sesson情報はsessionヘルパー関数を利用して取得することが可能です。すべての保存されている値を一括で見たい場合は下記で取得することが可能です。
session()->all();
Laravelで利用されるCookieの内容とSessionにはどのような情報が保存されるのが理解できたと思います。Session情報は認証にも深く関わってくるものなのでこの際にしっかりと理解しておきましょう。