LaravelのCSRF対策の処理を実際のコードから見てみる

# 初めに
Laravelを使用している方でLaravelの扱い方に慣れていないころ、`419エラー`に出会った方は割といるのではないかと思います。
今回はその`419エラー`を生み出しているLaravelのCSRF対策の仕組みついて実際のコードから見ていきます！

# 環境
|ツール|バージョン|
|:--|:--|
|PHP|7.4.8|
|Laravel|8.14.0|

# CSRF対策とは？
CSRF（クロスサイト・リクエスト・フォージェリ）とは、ざっくり言うと罠サイト等から他の人のブラウザのCookieに書かれているセッションIDを取得し、そのIDを使用するなどの方法で特定のWebアプリケーションへアクセスすることで、元のセッションのデータの持ち主がリクエストをしたように偽装することです。詳しくは[IPAの記事](https://www.ipa.go.jp/security/vuln/vuln_contents/csrf.html)を見てみて下さい。

## CSRF対策の一例
`体型的に学ぶ安全なWebアプリケーションの作り方　第二版　4.5節　「重要な処理」の際に混入する脆弱性`より

```php
入力画面
<?php
session_start();
if (empty($_SESSION['token'])) {
  $token = bin2hex(openssl_random_pseudo_bytes(24));
  $_SESSION['token'] = $token;
} else {
  $token = $_SESSION['token'];
}
?>

<form action="45-003a.php" method="POST">
新パスワード<input name="pwd" type="password"><br>
<input type="hidden" name="token" value="<?php echo htmlspecialchars($token, ENT_COMPAT, 'UTF-8');?>">
<input type="submit" value="パスワード変更">
</form>
```

```php
結果を映す画面
<?php
session_start();
$token = filter_input(INPUT_POST, 'token');
if (empty($_SESSION['token']) || $token !== $SESSION['token']) {
  die('正規の画面からご利用ください。'); //エラーメッセージ
}
//成功時の動作
?>
```

このプログラムが行っていることはこの３つです。
1. フォーム入力画面を作成時にPHPの暗号を生成する関数([openssl_random_pseudo_bytes](https://www.php.net/manual/ja/function.openssl-random-pseudo-bytes.php))を実行し、その結果をセッションへセットする。
2. セッションへセットしたデータはhtmlの`<form></form>`内に`token`という名前でセットし、他の入力データと共にHTTPのPOSTリクエスト実行から結果を表示するプログラムへ送る。
3. 入力画面から送られてきたデータのうち`token`の名前に入っているデータを取り出し、それと入力画面作成時にセッションへセットしたデータを比べ、同じであれば成功時の処理を、違えばそこで処理を終了する。
図で表すとこのようになります。
![CSRF対策の紹介.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/478883/a3f9a99b-59c8-9836-3d65-94d2dce5b87c.jpeg)

次にLaravel内でのCSRF対策の処理を見ていきます。

# LaraveのCSRF対策のコードを見てみる
## コードを見る前に
　LaravelのCSRF対策のコードは多数のクラスのメソッドによって成り立っているため、この章以降のコードを追うのみではCSRF対策の構造が分かりにくく、この記事自体を見る気が萎える方も出てしまう方も出てしまうかもしれません。なので、Laravelのコードを見る前にLaravelのデフォルトのCSRF対策のプログラムでやっていることと、そのプログラムの進行の図をここでお見せします。もし、この章以降のコードを追う中でCSRF対策の構造の理解がしにくいな、と感じた場合はここを見返してみて下さい。
　LaravelのCSRF対策でやっていることは、暗号作成時にその暗号の値を`/storage/framework/sessions/暗号の値`へファイルとして保存し、その暗号の照合時に、リクエスト内で`@csrf`などでhtmlの`<form></form>`内に`_token`という名前セットされたその暗号の値と作成時に作られたファイルに記された暗号の値を比べる、ということです。つまり、**LaravelのデフォルトのCSRF対策では`$_SESSION`は用いていない**ということです。
図で表すとこんな感じです。
![Laravel内のCSRF対策.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/478883/2aafc00e-6c53-dfde-3261-cbb0c27e1575.jpeg)

前振りはここまでにして、さっそくLaravelの実際のコードからCSRF対策の仕組みについて見ていきます。

## セッションをチェックしているコードを見てみる
今回はセッションをチェックしているコードのうち、デフォルトでセットされている`VerifyCsrfToken`に焦点を当てました。

### VerifyCsrfToken
名前空間　`Illuminate\Foundation\Http\Middleware`

```php
public function handle($request, Closure $next)
{
  if (
      $this->isReading($request) || // .....(1)
      $this->runningUnitTests() ||  // .....(2)
      $this->inExceptArray($request) ||  // .....(3)
      $this->tokensMatch($request)  // .....(4)
     ) {
         return tap($next($request), function ($response) use ($request) {
             if ($this->shouldAddXsrfTokenCookie()) {
                 $this->addCookieToResponse($request, $response);
             }
           });  // .....(5)
       }

       throw new TokenMismatchException('CSRF token mismatch.');
}
```
セッションのチェックを実行する`handle`メソッドをまずは見ていきます。
このメソッドで行われていることは、(1)～(4)の条件のどれかをクリアしたリクエストは(5)へ進み、それ以外は例外処理が行われ、エラーが表示されるということです。そして、(1)～(4)の条件は何なのかというと、
(1)　リクエストのHTTPメソッドが`HEAD`、`GET`、`OPTIONS`であれば`true`、それ以外は`false`
(2)　リクエストがユニットテスト内の物であれば`true`、それ以外は`false`
(3)　セッションのチェックの対象外のリクエスト(`App\Http\Middleware\VerifyCsrfToken`で設定できる)であれば`true`、それ以外は`false`
(4)　リクエストで送られてきたデータの中の`_token`の名前が与えられたデータとあらかじめ作成されたセッションデータが一致した場合`true`、そうではない場合は`false`

(5)の内容は、リクエストで送られてきたセッションデータがある場合はそれを`XSRF-TOKEN`という名前を付けたCookieへ書き込み、ない場合は新たにセッションのデータを作成し、それを書き込む感じです。これは処理を渡されると必ず実行され、その結果が次の処理へ渡されます。（理由は`$this->shouldAddXsrfTokenCookie`メソッドはコードを書き換えない限り必ず`true`を返すから）

先ほど見てきた(1)～(4)の条件の中で、セッションの確認を行っている(4)のメソッドについて次は見ていきます。
(4)を行っているメソッドは`tokensMatch`メソッドで、コードは以下のようになっています。

```php

protected function tokensMatch($request)
{
  $token = $this->getTokenFromRequest($request);
  
  return is_string($request->session()->token()) && is_string($token) &&
          hash_equals($request->session()->token(), $token); 
}
```
内容は非常にシンプルで、リクエストから送られてきた`$token`とリクエストにあらかじめセットされた`$request->session()->token()`が互いに文字列データであり一致している場合に`true`を返します。([hash_equals](https://www.php.net/manual/ja/function.hash-equals.php)を用いることでタイミング攻撃に対して安全に文字列データを比較できます。また、`$request->session()->token()`は後程あつかう`Illuminate\Session\Store`インスタンスの`token`メソッドの実行結果)

セッションの値の確認方法は分かりましたが、`$request->session()->token()`の値と比較する対象である`$token`がどのようにリクエストのデータから取得できるか謎なので、`getTokenFromRequest`メソッドについて調べていきます。

```php

protected function getTokenFromRequest($request)
{
  $token = $request->input('_token') ?: $request->header('X-CSRF-TOKEN'); //..(1)
  
  if (! $token && $header = $request->header('X-XSRF-TOKEN')) {
    $token = CookieValuePrefix::remove($this->encrypter->decrypt($header,
              static::serialized()));
  } //...(2)

  return $token;
}          
```
`getTokenFromRequest`メソッドでは以下の二つのことが行われています。
(1)　リクエストで送られてきた`_token`の名前の付いたデータの値かそれが空であれば、`X-CSRF-TOKEN`という名前のリクエストのヘッダーの値が代わりに入る。
(2)　`$token`の値が`null`かつ`X-XSRF-TOKEN`という名前のリクエストのヘッダーの値が存在し、`null`ではない場合に`X-XSRF-TOKEN`の値がうまく復号化できた場合にその値を`$token`に入れる。

`X-CSRF-TOKEN`と`X-XSRF-TOKEN`のデフォルトの値は`null`なので、もしリクエストの`_token`のデータが設定されていない場合は`getTokenFromRequest`メソッドは`null`を返し、その場合`tokensMatch`メソッドの`$request->session()->token()`と`$token`一致しないため、`tokensMatch`メソッドは`false`を返します。こういうわけで`@csrf`などを含めずにLaravelのPOSTメソッドを使ったリクエストを実行するとエラーが起こります。

LaravelのCSRF対策のためのセッションの比較の仕方は分かりましたが、セッションの生成の方法がまだ分かりません。そこで今度はデフォルト時のLaravelのセッションの生成の仕方を見てみます。

## セッションが作られる過程を見てみる
### StartSession
名前空間　`Illuminate\Session\Middleware`

`StartSession`は`Illuminate\Foundation\Http\kernel`クラスの`$middlewarePriority`配列の要素の一つです。ちなみにこの`kernel`クラスは`public/index.php`を見てもらえれば分かると思うのですが、Laravelのシステム全体が動くときの起点となります。
では最初に`handle`メソッドについて見ていきます。

```php
//初期設定
public function __construct(SessionManager $manager,
 callable $cacheFactoryResolver = null)
{
  $this->manager = $manager;　//SessionManagerはここでセットされている
  $this->cacheFactoryResolver = $cacheFactoryResolver;
}

public function handle($request, Closure $next)
{
  if (! $this->sessionConfigured()) {
      return $next($request);
  } //...(1)

  $session = $this->getSession($request); //...(2)
  
  if ($this->manager->shouldBlock() || 
     ($request->route() instanceof Route && $request->route()->locksFor())) {
      return $this->handleRequestWhileBlocking($request, $session, $next);
  } else {
      return $this->handleStatefulRequest($request, $session, $next);
  } //...(3)
}
```
このプログラムの内容はこの３つです。
(1)　もし`config\session.php`が存在しない、もしくは中身がない場合は`false`を返してクロージャの処理をするが、たいていのLaravelのアプリケーションの場合そんなことはないので、何もせずに次の処理へ
(2)　セッションを取得する。詳しくは後で見ていく。
(3)　もし`config\session.php`内の配列の`block`キーの値が`true`またはリクエストのURLの値から`Illuminate\Routing\Route`インスタンスになるかつセッションごとにURLのリクエスト制限がない時に`$this->handleRequestWhileBlocking($request, $session, $next)`は行われるが、それ以外は`$this->handleStatefulRequest($request, $session, $next);`が実行される。

`config\session.php`内の配列の`block`キーの値はたいてい設定ので`false`であり、セッションごとにURLのリクエスト制限はデフォルトではありません。よって`handle`メソッドは大抵の場合`$this->handleStatefulRequest($request, $session, $next);`を最終的に実行します。

全体の流れを見たところで、次はセッションの取得について見ていきます。

```php

public function getSession(Request $request)
{
  return tap($this->manager->driver(), function ($session) use ($request) {
    $session->setId($request->cookies->get($session->getName()));
  });
}
```
`getSession`で行っていることは、セッションを扱うインスタンス（`Illuminate\Session\Store`インスタンス、詳細は後で見ていく）をセットし、そのインスタンスへリクエストのCookieデータのうち`config\session.php`内の配列の`cookie`キーの値の名前が付けられたデータを代入します。

次は`handle`メソッドの処理の目玉である`handleStatefulRequest`について見ていきます。

```php

protected function handleStatefulRequest
(Request $request, $session, Closure $next)
{
  $request->setLaravelSession($this->startSession($request, $session)); //...(1)

  $this->collectGarbage($session); //...(2)

  $response = $next($request); //...(3)

  $this->storeCurrentUrl($request, $session); //...(4)

  $this->addCookieToResponse($response, $session); //...(5)

  $this->saveSession($request); //...(6)

  return $response;
}
```
`handleStatefulRequest`の内容はこの5つです。
(1)　リクエストのCookieデータに従ってセッションを設定し、`Illuminate\Session\Store`インスタンスを`Illuminate\HttpRequest`インスタンスへセットする。これを行うことで、`$request->session()`により`Store`インスタンスをいつでも呼び出せる。`Store`インスタンスについては後で見ていく。
(2)　古くなったセッションデータ（`config\session.php`内の配列の`lifetime`キーの値を超えているデータ）を削除する。
(3)　セッションと関りが浅いと思い全く調べていないので、処理の詳細は分からない。おそらく`Illuminate\HttpResponse`インスタンスを返しているのかな？
(4)　`storeCurrentUrl`の処理の説明は必要なときのために現在のURLを保存すると書いてあるが、これがどれほど重要かはいまいちわからない。GETリクエスト以外では効果なし。
(5)　レスポンスのヘッダーに、`config\session.php`内の配列の`cookie`キーの値の名前がついたCookieのデータをセットする。
(6)　セッションのデータを保存する。保存場所・方法については後で見ていく。

(1)～(6)の処理が終わった後、レスポンスのインスタンスを返します。
ちなみにセッションをセットする`startSession`の処理はこんな感じです。

```php
protected function startSession(Request $request, $session)
{
  return tap($session, function ($session) use ($request) {
    $session->setRequestOnHandler($request);　//デフォルトの状態では無視してよし

    $session->start();
  });
}
```
`Illuminate\Session\Store`インスタンスの`start`メソッドを実行し`Store`インスタンスを返します。この`start`メソッドについては後で見ていきます。

これまではセッションの作成過程を大まかにみてきましたが、次はセッションの作成・管理を行うインスタンスがどのようにセットされているのかを見ていきます。

### Manager(abstract class)
名前空間　`Illuminate\Support`
このManagerクラスは抽象クラスで、このクラス内にセッションを作成するなどの機能はなく、セッションや他のLaravelの機能を使用する際にそれぞれの機能を担うクラスたちをまとめる役割があります。


```php
public function driver($driver = null)
{
  $driver = $driver ?: $this->getDefaultDriver(); //...(1)

  if (is_null($driver)) {
    throw new InvalidArgumentException(sprintf(
    'Unable to resolve NULL driver for [%s].', static::class));
  } //...(2)

  if (! isset($this->drivers[$driver])) {
    $this->drivers[$driver] = $this->createDriver($driver);
  } //...(3)

  return $this->drivers[$driver];
}
```
このメソッドで行っていることは以下の三つです。
(1)このメソッドの引数にインスタンスが指定されていればそれを`$driver`へ入れ、引数が指定されていない場合は、継承先のクラスに存在する`getDefaultDriver`メソッドの結果を入れる。デフォルトでは引数は`null`なため、`getDefaultDriver`メソッドの結果が`$driver`へ入れられる。`getDefaultDriver`メソッドについては後で説明する。
(2)もし`$driver`の値が`null`出会った時に例外処理が行われる。
(3)もし`$this->drivers[$driver]`に値が既にある場合は何もせず、ない場合は`$driver`の値を`createDriver`メソッドへ渡し、その結果を`$this->drivers[$driver]`へ入れる。

(1)~(4)が終了した後、`$this->drivers[$driver]`の値を返します。
次にこのメソッドの(3)に登場した`createDriver`メソッドについて見ていきます。

```php
protected function createDriver($driver)
{
  if (isset($this->customCreators[$driver])) {
    return $this->callCustomCreator($driver); //...(1)
  } else {
    $method = 'create'.Str::studly($driver).'Driver'; //...(2)
    
    if (method_exists($this, $method)) {
      return $this->$method(); //...(3)
    }
  }

  throw new InvalidArgumentException("Driver [$driver] not supported.");
}
```
このメソッドで行っていることは以下の三つです。
(1)もし`$this->customCreators[$driver]`に値がセットされている場合は`callCustomCreator`メソッドの結果を返すが、あらかじめ`$this->customCreators[$driver]`に値がセットされていることはないのでここでは扱わない。
(2)あらかじめ設定されている文字列とLaravelのhelperメソッドの一つである[Str::studlyメソッド](https://laravel.com/docs/8.x/helpers#method-studly-case)の引数へ`$driver`の値を入れるた結果の値を結合した値を`$method`へ入れる。`getDefaultDriver`メソッドを紹介する時に説明するが、`$driver`の値は必ず文字列データになり、その値が[Str::studlyメソッド](https://laravel.com/docs/8.x/helpers#method-studly-case)によってそのデータの頭文字が大文字になる。
(3)[method_exists](https://www.php.net/manual/ja/function.method-exists.php)のよってこのクラスまたは継承先のクラスに`$method`の名前を持ったメソッドが存在する場合はその結果を返す。もし存在しない場合は例外処理が行われる。

次は`Manager`クラスの継承先の一つであり、`getDefaultDriver`メソッドと`$method`の名前を持ったメソッドを有している`SessionManager`クラスについて見ていきます。

### SessionManager(Managerを継承)
名前空間　`Illuminate\Session`

```php
public function getDefaultDriver()
{
  return $this->config->get('session.driver');
}
```
このメソッドは継承元であるManagerクラスの`createDriver`メソッドの引数である`$driver`の値を作成してます。
このメソッドは`/config/session.php`の配列内の`driver`要素の値を返します。この要素のデフォルトの値は'file'という文字データなので、`createDriver`メソッドの引数である`$driver`の値はデフォルトでは必ず文字列データになり、どうメソッド内の`$method`の値は`createFileDriver`になります。

先ほど見たように継承元であるManagerクラスの`createDriver`メソッドは継承元かこのクラスの`$method`の値のメソッドの結果を返します。さきほど`$method`の値は`createFileDriver`ということが分かったので、今度はこのクラス内の`createFileDriver`メソッドについて見ていきます。

```php
protected function createFileDriver()
{
  return $this->createNativeDriver();
}

protected function createNativeDriver()
{
  $lifetime = $this->config->get('session.lifetime'); //...(1)
  
  return $this->buildSession(new FileSessionHandler(
  $this->container->make('files'), $this->config->get('session.files'), 
   $lifetime)); //...(3)
}
```
`createFileDriver`メソッドは同クラス内の`createNativeDriver`メソッドの結果を返します。ということで`createNativeDriver`メソッドについて見ていきます。
`createNativeDriver`メソッドの内容は以下の二つです。
(1)`/config/session.php`の配列内の`lifetime`要素の値を`$lifetime`へ入れる。(デフォルト値は120)
(2)`$this->container->make('files')`で得られる、ファイル書き込み機能を実装した`Illuminate\Filesystem\Filesystem`インスタンスと`/config/session.php`の配列内の`files`要素の値である`/storage/framework/sessions/`までのパス名を引数として渡したセッションの値をファイルへ書きこむ機能を担う`FileSessionHandler`インスタンスを同クラス内の`buildSession`メソッドへ渡し、その結果を返す。

`buildSession`メソッドとはLaravelのセッションの様々な操作が書かれているクラスのインスタンスを返すメソッドなのですが、このメソッドについて掘り下げていきます。

```php
protected function buildSession($handler)
{
  return $this->config->get('session.encrypt')
    ? $this->buildEncryptedSession($handler)
    : new Store($this->config->get('session.cookie'), $handler);
}
```
`buildSession`メソッドは、`/config/session.php`の配列内の`encrypt`要素の値が`false`の場合はセッションの操作を行うプログラムが書かれた`Illuminate\Session\Store`クラスの引数に先ほどの`FileSessionHandler`インスタンスを入れたインスタンスが返され、`true`の場合は`Store`クラスに`$this->container['encrypter']`で得られる`Illuminate\Encryption\Encrypter`クラスの機能が合わさった`Illuminate\Session\EncryptedStore`インスタンスが返されます。デフォルトの場合の`encrypt`要素の値は`false`なので`Store`インスタンスが返されます。

## いったんここまでの流れをまとめる（読み飛ばしても大丈夫）
ここまでCSRF対策を行う`VerifyCsrfToken`クラスや`StartSession`クラスのコードを見ながら、Laravel内のCSRF対策やセッションの作成過程を見てきましたが、セッションの作成やセッションの内容のファイルへの書きこみ、取り出しの過程をPHPでどのように実現させているのかをまだ見ていません。なので、次はトークンの作成と管理の機能がPHPのコードにより実装されているクラスを見ていくのですが、そのクラスの理解を容易にするためにセッションの作成過程が書かれている`StartSession`の`handle`メソッドと`handleStatefulRequest`メソッドで行われることの流れとその結果をおさらいします。

`handle`メソッドの流れとそれぞれの工程の結果はこんな感じでした。

- 送られてきたリクエストのCookieのからセッションのデータを取り出す。⇒　`Illuminate\Session\Store`インスタンスの`setId`メソッドを実行したうえで、`Store`インスタンスを呼び出す。
- `Illuminate\Http\Request`インスタンスと`Store`インスタンスを`handleStatefulRequest`メソッドへ渡す。

`handleStatefulRequest`メソッドの流れとそれぞれの工程の結果はこんな感じでした。

- 同クラス内の`startSession`メソッドでセッションを作成する。⇒　`Store`インスタンスの`start`メソッドを実行
- 期限切れのセッションを削除する。⇒ `Store`インスタンスの`getHandler`メソッドを実行
- 作成したセッションを保存する。⇒　`Store`インスタンスの`save`メソッドを実行

上の二つのメソッドの結果を見てわかる通り、セッションの作成には`Illuminate\Session\Store`インスタンスが密接にかかわっています。ということで次は`Store`インスタンスの元である`Illuminate\Session\Store`クラスについて見ていきます。

### Store
名前空間　`Illuminate\Session`

```php
//初期設定
public function __construct($name, SessionHandlerInterface $handler, $id = null)
{
  $this->setId($id);
  $this->name = $name;
  $this->handler = $handler;
}
```
`Store`クラスのコンストラクトメソッドは、`StartSession`クラスの`getSession`メソッド実行時、ひいては`Manager`クラスの`driver`メソッド実行時に`Store`インスタンス作成した際に呼び出されます。その時の`$name`の値は`$this->config->get('session.cookie')`、`$handler`の値は`Illuminate\Session\FileSessionHandler`インスタンスとなります。（ここら辺は`SessionManager`クラスの説明の中で書かいています。）引数`$id`は指定されていないので、`$this->id`にはランダムな文字列データが入れられますが、`getSession`メソッドの実行中に`$this->id`は書き換えられるので、気にしなくて良いです。

```php
public function setId($id)
{
  $this->id = $this->isValidId($id) ? $id : $this->generateSessionId();
}

public function isValidId($id)
{
  return is_string($id) && ctype_alnum($id) && strlen($id) === 40;
}

protected function generateSessionId()
{
  return Str::random(40);
}
```
`StartSession`クラスの`getSession`メソッドで呼び出されるこのクラス内の`setId`メソッドは、引数の値がセッションの形式に沿っているか確かめ、もし沿っているのであればその値を返し、ダメであれば新たにセッションIDを作成し、`$this->id`へその値を入れます。
セッションの形式の確認は同クラス内の`isVaildId`メソッドで行っており、このメソッドでやっていることは、文字列データかつ英数字かつ文字数が40文字であれば`true`、そうでなければ`false`を返します。（詳しくは[is_string](https://www.php.net/manual/ja/function.is-string)、[ctype_alnum](https://www.php.net/manual/ja/function.ctype-alnum)、[strlen](https://www.php.net/manual/ja/function.strlen)を見て下さい。）
セッションIDの作成は同クラス内の`generateSessionId`メソッドで行っています。このメソッドの内容はただ単に[Str::random(40)](https://laravel.com/docs/8.x/helpers#method-str-random)で作成された40時のランダムな文字列データを返します。ちなみに`Str::random()`には[random_bytes](https://www.php.net/manual/ja/function.random-bytes)が使われているようです。

```php
public function start()
{
  $this->loadSession(); //...(1)
  
  if (! $this->has('_token')) {
    $this->regenerateToken();
  } //...(2)

  return $this->started = true; //...(3)
}
```
`StartSession`クラスの`startSession`メソッドで実行される`start`メソッドで行われることは以下の三つです。
(1)セッション情報が書かれたファイルを取り出し、その値を`$this->attributes`配列へ入れる。詳しくは後で見ていく。
(2)もし`$this->attributes`配列に'_token'がキーとなる値が存在しない場合同クラス内の`regenerateToken`メソッドを実行。`regenerateToken`メソッドについては後で見ていく。
(3)最後に`$this->started`に`true`を入れる。`$this->started`はセッションの様々な操作に使われているようだが、どのように使われているかまでは調べきれなかったのでわからない。

次は`start`メソッドで一番初めに実行される`loadSession`について見ていきます。

```php
protected function loadSession()
{
  $this->attributes = array_merge($this->attributes, $this->readFromHandler());
}

protected function readFromHandler()
{
  if ($data = $this->handler->read($this->getId())) { //......(1)
    $data = @unserialize($this->prepareForUnserialize($data)); //......(2)
    
    if ($data !== false && ! is_null($data) && is_array($data)) {
      return $data; //......(3)
    }
  }

  return [];
}

public function getId()
{
  return $this->id;
}
```
`loadSession`メソッドでは、同クラス内の`readFromHandler`メソッドの結果を`$this->attributes`へ加えています。そして、そのメソッド内で実行されている`readFromHandler`メソッドは以下の三つのことを行っています。
(1)同クラスの`setId`メソッドで`$this->id`へセットされた値を`getId`メソッドで呼び出し、その値を引数にとり`FileSessionHandler`インスタンスの`read`メソッドが実行される。`raed`メソッドによりもし`$this->id`の値と同じ名前のファイルが存在する場合、そのファイルの内容を`$data`へ入れて(2)以降を実行し、存在しなければ文字列データの空の値(`''`)を`$data`へ入れ、何もせず空の配列を返す。
(2)ファイルから取得したセッションの情報は作成時に保存用の表現としてシリアル化されているので、[unserialize](https://www.php.net/manual/ja/function.unserialize.php)メソッドでPHPで扱える値に復元する。(`prepareForUnserialize`メソッドはただ引数を返すだけ)
(3)もしデータがうまく復元されなかったり、`null`であったり、文字列データではない場合は空の配列を返し、そうでなければ`$data`の値をそのまま返す。

今度は、`loadSession`メソッドが終了した後に行われる`has`メソッドについて見ていきます。

```php
public function has($key)
{
  return ! collect(is_array($key) ? $key : func_get_args())
             ->contains(function ($key) 
  {
    return is_null($this->get($key));
  });
}

public function get($key, $default = null)
{
  return Arr::get($this->attributes, $key, $default);
}
```
`has`メソッドでは[collect](https://laravel.com/docs/8.x/collections#method-collect)メソッドを使用して`Illuminate\Support\Collection`インスタンスを呼び出し、そのインスタンスのメソッドである`contains`メソッドにより引数の`$key`がキーとなる値が`$this->attributes`にある場合は`true`、ない場合は`false`を返します。`contains`メソッドの引数内で実行されている`get`メソッド[Arr::get](https://laravel.com/docs/8.x/helpers#method-array-get)によって`$this->attributes`に引数`$key`の値をキーにとる値がある場合はその値を返し、ない場合は空の文字列データを返します。

次にもし`has`メソッドの結果が`true`の場合（ブラウザのクッキーにセッションが存在しない状態でリクエストが来た場合）に行われる`regenerateToken`メソッドとそのメソッド内で実行される`put`メソッドを見ていきます。

```php
public function regenerateToken()
{
  $this->put('_token', Str::random(40));
}

public function put($key, $value = null)
{
  if (! is_array($key)) {
    $key = [$key => $value];
  }

  foreach ($key as $arrayKey => $arrayValue) {
    Arr::set($this->attributes, $arrayKey, $arrayValue);
  }
}
```
`regenerateToken`メソッドは`generateSessionId`メソッドと同じようにランダムな文字列データを作成し、その値を同クラス内の`put`メソッドで`_token`をキーとして`$this->attributes`にセットします。`put`メソッドは引数の`$key`をキーとし、`$value`をその値として[Arr::set](https://laravel.com/docs/8.x/helpers#method-array-set)により`$this->attributes`へセットするか、`$key`自体が配列の場合はそれを`$this->attributes`へセットします。

この次は`handleStatefulRequest`メソッドの最後に実行される`save`メソッドを見ていきます。

```php
public function save()
{
  $this->ageFlashData();
  
  $this->handler->write($this->getId(), $this->prepareForStorage(
    serialize($this->attributes)));

  $this->started = false;
}
```
このメソッドでは、`FileSessionHandler`インスタンスの`write`メソッドにより、リクエストのクッキーのセッションIDの値(なければ新たに作成)を名前にとるファイルを新たに作成し、それに`$this->attributes`を[serialize](https://www.php.net/manual/ja/function.serialize)でシリアル化させた値を書き込みます。そして最後におそらくセッションの処理が終わったことを知らせるために`$this->started = false`を行います。

そして、最後に後で説明するヘルパーメソッドである`csrf_token`メソッドで実行される`token`メソッドについて説明していきます。

```php
public function token()
{
  return $this->get('_token');
}
```
このメソッドの説明は簡潔で、`get`メソッドにより`_token`をキーにとる値が`$this->attributes`に存在すればその値を返し、なければ空の文字列データを返します。`$this->attributes[_token]`が存在しないことはデフォルトではまずないので、何かしらの文字列データを返します。

`Store`クラスの中で`FileSessionHandler`インスタンスが何回か登場したので、今度は`FileSessionHandler`クラスについて見ていきます。

### FileSessionHandler
名前空間：`Illuminate\Session`

```php
public function __construct(Filesystem $files, $path, $minutes)
{
  $this->path = $path;
  $this->files = $files;
  $this->minutes = $minutes;
}
```
このクラスのコンストラクトメソッドは`SessionManager`クラスの`buildSession`メソッドの中で実行されます。この時の引数`$file`には`$this->container->make('files')`（`Illuminate\Filesystem\Filesystem`インスタンス）, 引数`$path`には`$this->config->get('session.files')`（`/config/session.php`の`file`要素の値）、引数`$minutes`には`$this->config->get('session.lifetime')`（`/config/session.php`の`lifetime`要素の値）が入ります。
次は`Store`クラスの`readFromHandler`メソッド内で実行される`read`メソッドについて見ていきます。

```php
public function read($sessionId)
{
  if ($this->files->isFile($path = $this->path.'/'.$sessionId)) { //...(1)
    if ($this->files->lastModified($path) >= Carbon::now()
         ->subMinutes($this->minutes)->getTimestamp()) { //...(2)
      return $this->files->sharedGet($path); //...(3)
    }
  }

  return '';
}
```
`read`メソッドでは以下の三つのことを行っています。

(1)`Illuminate\Filesystem\Filesystem`クラスの`isFile`メソッドで`$this->path.'/'.$sessionId`の値のパスとファイル名を持つファイルがないか検証し、ある場合は`$path`へそのファイルのパスとファイル名を入れて次の処理へ進み、ない場合は空の文字列データを返す。
(2)`Filesystem`クラスの`lastModified`メソッドにより(1)で取得した情報からそのファイルが最後に書き込まれた時間を割り出し、もしその値が`Carbon\Carbon`クラスの`subMinutes`メソッドなどを使用し、現在の時刻から`/config/session.php`の`lifetime`要素の値を引いた数以上だった場合は次の処理へ進み、そうでない場合は空の文字列データを返す。`lastModified`メソッドと`getTimestamp`メソッド（こちらはおそらく）はint型のUnix タイムスタンプで時刻を返し、Unix タイムスタンプは時間が進むほど大きくなる。つまり、ここで行っていることを言い換えるとファイルの作成時刻とリクエストが発生した時刻の差分が`lifetime`要素の値より小さい（新しい）場合は次の処理、大きい（古い）場合は空の文字列データを返す、ということになる。
(3)ここでは`$path`を引数にとり`Filesystem`クラスの`sharedGet`メソッドを実行する。`sharedGet`メソッドについては後で見ていく。

最後に`Store`クラスの`save`メソッドで実行される`write`メソッドを見ていきます。

```php
public function write($sessionId, $data)
{
  $this->files->put($this->path.'/'.$sessionId, $data, true);

  return true;
}
```
といってもこのメソッドは単に、`$this->config->get('session.files')`の値と引数`$sessionId`の値を組み合わせたものと引数`$data`を`Filesystem`クラスの`put`メソッドへ渡し、`true`を返すということを行います。`put`メソッドについては後で見ていきます。

次はこのクラスでたびたび出てきた`Filesystem`クラスについて見ていきます。

### Filesystem
名前空間：`Illuminate\Filesystem`

```php
public function isFile($file)
{
  return is_file($file);
}
```
このメソッドは[is_file](https://www.php.net/manual/ja/function.is-file)の結果を返します。もし引数の`$file`が通常のファイルなら`true`を返し、ディレクトリなどそれ以外なら`false`を返します。

```php
public function lastModified($path)
{
  return filemtime($path);
}
```
このメソッドは[filemtime](https://www.php.net/manual/ja/function.filemtime)メソッドの結果を返します。引数`$path`が最後に書き込まれた時刻をint型のUnix タイムスタンプで返します。失敗した時は警告が発生します。

```php
public function sharedGet($path)
{
  $contents = ''; //...(1)

  $handle = fopen($path, 'rb'); //...(2)
  
  if ($handle) { //...(3)
    try {
      if (flock($handle, LOCK_SH)) { //...(4)
        clearstatcache(true, $path); //...(5)
        
        $contents = fread($handle, $this->size($path) ?: 1); //...(6)

        flock($handle, LOCK_UN); //...(7)
      }
    } finally {
      fclose($handle); //...(8)
    }
  }
  
  return $contents; //...(9)
}
```
このメソッドで行うことは以下の9つです。

(1)`$contents`の初期化
(2)[fopen](https://www.php.net/manual/ja/function.fopen.php)メソッドにより、`$path`のファイルのポインタリソースを返す。つまり、対象のファイル中の動作をする場所を返している。もっと知りたい方は[こちら](https://teratail.com/questions/180539)（PHPのポインタについてはまだ勉強不足なので、ここについて詳しく書けない。）ちなみに`fopen`メソッドの第二引数は行う動作を決めるものであり、`rb`はバイナリモードでファイルを読み込むという動作を表している。バイナリモードについては勉強不足で説明できない。（教えて頂けるとありがたいです。）
(3)`fopen`メソッドは失敗した場合に`false`を返すので、失敗した時だけ(9)へ進み、それ以外は次の処理へ進む。
(4)[flock](https://www.php.net/manual/ja/function.flock)メソッドとは、第一引数のポインタリソースを持つファイルの状態の変更を止めたり、他のプログラムからの参照を止めることで競合するプログラムが存在してもアプリケーション全体を問題なく動作させる機能を持つ。ちなみに第二引数の`LOCK_SH`によって、他のプログラムから同じファイルのデータを参照できるが、変更できなくさせている。もしぴんと来なければ、[こちらの記事](http://phpspot.net/php/pg%E3%83%95%E3%82%A1%E3%82%A4%E3%83%AB%E3%83%AD%E3%83%83%E3%82%AF%E3%81%AB%E3%81%A4%E3%81%84%E3%81%A6.html)をどうぞ。
(5)[clearstatcache](https://www.php.net/manual/ja/function.clearstatcache.php)メソッドは第一引数を`true`にすることで第二引数のファイルのキャッシュ値を削除する機能を持つ。データが頻繁に変化するファイルを扱うときに便利。
(6)[fread](https://www.php.net/manual/ja/function.fread.php)で`fopen`メソッドで参照するファイルのリソースポインタから同クラスの`size`メソッド内の[filesize](https://www.php.net/manual/ja/function.filesize.php)メソッドで入手したファイルのサイズ分（単位はバイト）の内容を読み込み、その結果を`$contents`へ入れる。もしファイルサイズが読み取れなかった場合は1バイト分読み込む。
(7)ファイルの読み込みが終わった後、そのままでは他のプログラムが読み込んだファイルの変更ができないため第二引数を`LOCK_UN`にした`flock`メソッドによって、ファイルを再び自由に変更可能にします。
(8)`fopen`メソッドで参照したファイルのリソースポインタを閉じる動作として[fclose](https://www.php.net/manual/ja/function.fclose)メソッドを行う。ちなみにこのメソッドを書かなくても`fopen`メソッドで参照したファイルのリソースポインタは勝手に閉じられる。
(9)`$contents`の値を返す。

ここで、なぜ[file-get-contents](https://www.php.net/manual/ja/function.file-get-contents)メソッドを使用せず、`fread`メソッドを使用しているのか疑問に思う方もいるかと思います。なぜ`fread`メソッドを使用するのかの解答は（調べてなくて）分かっていませんが、おそらくバイナリセーフな`fread`メソッドを使用することでNullバイト攻撃を防ぐためだと思います。バイナリセーフ、Nullバイト攻撃については[こちらの記事](https://qiita.com/dodonki1223/items/e3d2c4a032b129e1f6ce)をどうぞ。

最後に`Store`クラスの`save`メソッドで使用された`put`メソッドを見ていきます。

```php
public function put($path, $contents, $lock = false)
{
  return file_put_contents($path, $contents, $lock ? LOCK_EX : 0);
}
```
このメソッドで行われていることは、[file_put_contents](https://www.php.net/manual/ja/function.file-put-contents)メソッドで引数`$path`に書かれているファイルへ`$contents`のデータを書き込みます。`Store`クラスの`save`メソッドにより`$lock=true`となるため、`file_put_contents`メソッドは他のプログラムによる書き込むファイルの読み込みと変更を禁止する状態(これを排他ロックというらしい)で実行されます。`file_put_contents`メソッドが成功した場合はファイルへ書き込んだデータの量（バイト数）が返され、失敗した場合は`false`が返されます。

一番最後に`$request->input('_token')`の値を作成するヘルパーメソッドの`csrf_token`メソッドを見ていきます。

### csrf_token(helper関数)
ファイル場所　`laravel/framework/src/Illuminate/Foundation/`

```php
if (! function_exists('csrf_token')) {
  /**
   * Get the CSRF token value.
   * @return string
   * @throws \RuntimeException
   * /
   function csrf_token()
   {
     $session = app('session');
     if (isset($session)) {
       return $session->token();
     }

     throw new RuntimeException('Application session store not set.');
    }
}
```
このメソッドはもし`session`の解決で`Store`インスタンスが得られた場合にそのインスタンスの`token`メソッドの結果を返します。もし何も得られなかった場合は例外処理に映ります。`token`メソッドについては`Store`クラスを見ていく中で取り上げているので、そちらを見て下さい。`session`の解決が`Store`インスタンスを返す根拠は明確に見つかったわけではありませんが、同じヘルパーメソッドの`session`メソッドのコードのPHPDocの返り値の部分が`@return mixed|\Illuminate\Session\Store|\Illuminate\Session\SessionManager`になっていたのに対し、実際に返している値が`app('session')`だからです。~~ここについては今後もう少し調べていきます。~~　実際に`app('session')`をLaravelのヘルパ関数である[`dd`メソッド](https://laravel.com/docs/8.x/collections#method-dd)をかけて解析したところ、`\Illuminate\Session\SessionManager`インスタンスを返しました。

# 終わりに
ここまで見てくれてありがとうございます、そしてお疲れ様です。
今回のコードリーディングで、Laravelと一般的なコードでCSRF対策のために行っていることの**根本は変わらない**ということが知れて良かったです。
しかし、詳しく調べる気力が出なかったので、この記事の中には「きっとこんな感じだろう」という憶測で書いてしまっている部分もあります。今後もLaravelのコードを読むことで、このような部分を潰していきたいと思います。もし気になる、疑問に思うところがあれば指摘してくださるとありがたいです。
今後もLaravel内で気になることがあれば、こんな風にコードリーディングをできたらなーと思います。

ちなみにこんな記事も書いてます。良かったらぜひ
[Laravelのサービスコンテナのバインドと解決の仕組みが知りたい！](https://qiita.com/SanQ/items/a08f200614718315a63b)
[Laravelのsingletonメソッドの機能とその仕組みについて](https://qiita.com/SanQ/items/d927f6e887b3f531dc6c)