SPAにおけるLaravelのCORS設定周辺でつまずいた

# 概要
筆者はエンジニア歴がまだ1年経っていないphpがメインのシステムエンジニアです。
普段の実務ではLaravelは使用しておりません。そのため、不備等がある場合は教えて頂けると幸いです。
今回は友人とSPA開発する際にCORSの設定周辺で苦労したので、今後の自分も含めて共有させていただきます。

# そもそもCORSとは
調べれば詳細が記載されている記事がたくさん見つかると思うので大枠で説明すると、
同一生成元ではないドメインへリクエストの安全性を保証する仕組みです。
CORSに基づいた方法で実装すれば、同一生成元ではないところでもJavaScriptでアクセスすることが可能になると言うことです。
認識違いがあれば指摘していただきたいです。

# 環境
PHP 7.4.16
Laravel Framework 6.18.40

# 発生したエラー
SPAで画像をアップロードする機能を実装したところ、下記のエラーが出力されました...。

````
Access to XMLHttpRequest at 'http://localhost:8000/api/upload' from origin 'http://localhost:8080' has been blocked by CORS policy: Response to preflight request doesn't pass access control check: No 'Access-Control-Allow-Origin' header is present on the requested resource.
````

CORSの存在は知っていたのですが、まず自分自身`localhost`の部分が同じであればポート番号が違くても同一生成元だと思っていました...。
お恥ずかしい...。
その他のクロスドメインの判定としては、下記が挙げられます。
- プロトコルが異なる。 httpsとhttpの違い
- ホスト名が異なる。

# エラー解決までの過程
それでは上記のエラーを解決するのに、Laravel側でCORSの設定をしていきます。
自分は`fruitcake/laravel-cors`を利用しました。詳細は下記のリンクからご確認ください。
https://github.com/fruitcake/laravel-cors

````
# インストール
$ composer require fruitcake/laravel-cors
# メモリ周辺でエラーが発生する場合は下記のコマンドを実行。
$ COMPOSER_MEMORY_LIMIT=-1 composer require fruitcake/laravel-cors

# configファイルを作成。
$ php artisan vendor:publish --tag="cors"
````

configファイルは`/config/cors.php`に作成されます。
自分は下記のように各設定をしました。

```` cors.php
<?php

return [

    // CORSを設定するURI
    'paths' => ['api/*', 'sanctum/csrf-cookie'],

    // 許可するリクエストメソッド
    'allowed_methods' => ['*'],

    // 許可するリクエストオリジンの設定
    'allowed_origins' => ['http://localhost:8080'],

    'allowed_origins_patterns' => [],

    'allowed_headers' => ['*'],

    'exposed_headers' => [],

    'max_age' => 0,

    // Access-Control-Allow-Credentialsヘッダーを設定する。
    'supports_credentials' => true,
];
````

そして、それらをグローバルミドルウェアに追加しました。

```` /app/Http/Kernel.php
protected $middleware = [
    \App\Http\Middleware\TrustProxies::class,
    \App\Http\Middleware\CheckForMaintenanceMode::class,
    \Illuminate\Foundation\Http\Middleware\ValidatePostSize::class,
    \App\Http\Middleware\TrimStrings::class,
    \Illuminate\Foundation\Http\Middleware\ConvertEmptyStringsToNull::class,
    \Fruitcake\Cors\HandleCors::class, // 追加
];
````

しかし、また別のエラーが出力されました...。

````
// コンソール
POST http://localhost:8000/api/upload 419 (unknown status)

// レスポンスメッセージ
"message": "CSRF token mismatch.",
````

どうしてー！自分の認識だと、`api.php`に定義したルートはCSRFの保護は無効になると思っていました。
色々原因を探ったところ、以前実装した`sanctum認証`で設定したミドルウェアが原因だと判明しました。

```` /app/Http/Kernel.php
'api' => [
    EnsureFrontendRequestsAreStateful::class, // ← これ
    'throttle:api',
    \Illuminate\Routing\Middleware\SubstituteBindings::class
],
````

その中身を見ていると、下記の記述がありました。
だから、APIとして定義したルートでもCSRFのチェックがされていたのですね...。

```` EnsureFrontendRequestsAreStateful.php
public function handle($request, $next)
{
    $this->configureSecureCookieSessions();

    return (new Pipeline(app()))->send($request)->through(static::fromFrontend($request) ? [
        function ($request, $next) {
            $request->attributes->set('sanctum', true);

            return $next($request);
        },
        config('sanctum.middleware.encrypt_cookies', \Illuminate\Cookie\Middleware\EncryptCookies::class),
        \Illuminate\Cookie\Middleware\AddQueuedCookiesToResponse::class,
        \Illuminate\Session\Middleware\StartSession::class,
        config('sanctum.middleware.verify_csrf_token', \Illuminate\Foundation\Http\Middleware\VerifyCsrfToken::class), // ここでCSRFの保護が有効になってる！！
    ] : [])->then(function ($request) use ($next) {
        return $next($request);
    });
}
````

簡単にまとめると、
1. sanctum認証で必要なミドルウェアをapi.phpのルート全てに反映されてしまっていた。
2. それによって、CSRFのチェックが不要なルートにもCSRFの保護が有効になっていた。
といった感じです。

# エラー対策
上記の原因が判明したとなるとあとは簡単でした。
認証がいらないルートの場合はCSRFの保護を無効にすればいいので、`/app/Http/Middleware/VerifyCsrfToken.php`の`except`箇所で無効にするURIを設定します。
※ 無効にしたURIはCSRF対策で用いられるトークンがCookieに設定されなくなるので注意！！

```` /app/Http/Middleware/VerifyCsrfToken.php
<?php

namespace App\Http\Middleware;

use Illuminate\Foundation\Http\Middleware\VerifyCsrfToken as Middleware;

class VerifyCsrfToken extends Middleware
{
    /**
     * Indicates whether the XSRF-TOKEN cookie should be set on the response.
     *
     * @var bool
     */
    protected $addHttpCookie = true;

    /**
     * The URIs that should be excluded from CSRF verification.
     *
     * @var array
     */
    protected $except = [
        // ここに無効にするURIを追加する。
        'api/user/register',
    ];
}
````

# おわりに
今回SPAにおけるLaravel側のみのCORSの設定などを主に説明させていただきました。
また、筆者自身まだ未熟であるので説明不足であったり、理解不足などありましたらご指摘していただけると幸いです。