LaravelでのCORS対策とmiddlewareへの理解

LaravelでCORS対策の勉強をしていくつかの方法を知ったのでCORSへの理解と共にまとめる。

#そもそもCORSとは？
CORSとは `Cross-Origin Resource Sharing` の略で日本語だとオリジン間リソース共有 (CORS)と言われる。
同一オリジンポリシーを超えて通信を行うことをクロスオリジン間通信といい、その中でリソースの共有、つまりデータを共有することをCORSという。

###オリジンとは何か？
ではオリジンとは何かというと、スキーム + ホスト + ポート番号で構成される組み合わせのことである。
この組み合わせが一致すると同一オリジンと見なされて、一つでも一致しないとクロスオリジンとみなされる。
ドメインと似ているが、プロトコルとポート番号を含んでいる点がドメインとは異なる。
![origin.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/926931/90ca6fc3-b66e-53ad-6855-4be3586917b0.png)

アクセス元のオリジンが同一オリジンの場合、レスポンスヘッダの有無や内容にかかわらず通信は成功する。
しかし、クロスオリジン間通信では同一オリジンポリシーという制約が発生する。この設定を調整して、__異なるオリジンへアクセスできるように CORSを設定することをCORS対策__という。

レスポンスを受けるサーバー側ではリクエストを受ける下記レスポンスヘッダによってクロスオリジン通信の可否が左右される。
 
- [Access-Control-Allow-Origin](https://developer.mozilla.org/ja/docs/Web/HTTP/Headers/Access-Control-Allow-Origin)
-  [Access-Control-Allow-Credentials](https://developer.mozilla.org/ja/docs/Web/HTTP/Headers/Access-Control-Allow-Credentials)
-  [Access-Control-Expose-Headers](https://developer.mozilla.org/ja/docs/Web/HTTP/Headers/Access-Control-Expose-Headers)
-  [Access-Control-Max-Age](https://developer.mozilla.org/ja/docs/Web/HTTP/Headers/Access-Control-Max-Age)
-  [Access-Control-Allow-Methods](https://developer.mozilla.org/ja/docs/Web/HTTP/Headers/Access-Control-Allow-Methods)
-  [Access-Control-Allow-Headers](https://developer.mozilla.org/ja/docs/Web/HTTP/Headers/Access-Control-Allow-Headers)

##シンプルリクエストとプリフライトリクエスト
このクロスオリジン間通信ではシンプルリクエストとプリフライトリクエストというブラウザからのリクエストが飛ばされ、これらがあることでクロスオリジン間通信、つまりブラウザはサーバーからレスポンスを得ることができる。

###シンプルリクエスト
シンプルリクエストであるかどうかは以下の__全ての条件__に当てはまるかどうかである。
シンプルリクエストの時はプリフライトリクエストを引き起こさず、外部との通信が可能である。

- リクエストのメソッドが GET, POST, HEAD のいずれかに設定されている場合
-   Content-Type ヘッダーが以下の設定である時
 - application / x-www-form-urlencoded
 - multipart / form-data
 - text / plain

-   特定のHTTPリクエストヘッダが含まれない （詳細はこちら→[simple_requests](https://developer.mozilla.org/ja/docs/Web/HTTP/CORS#simple_requests)）
 -   Accept
 -   Accept-Language
 -   Content-Language
 -   DPR
 -   Downlink
 -   Save-Data
 -   Viewport-Width
 -   Width


###プリフライトリクエスト
プリフライトリクエストはシンプルリクエストではない時に出るリクエストである。その本リクエストを送信しても安全かどうかを確かめるいわゆる毒味のようなリクエストであり、OPTIONS メソッドを用いた通信で安全かどうかの確認をする。

###通信が失敗するとき・・・
クロスオリジン通信ではレスポンスヘッダの オリジン とリクエスト送信元オリジンが一致しないとブラウザは通信を失敗する。要するに同一オリジンポリシーに反しているということである。この時の多くのパターンとして2パターンある。

1.プリフライトリクエスト（本リクエスト前）により相互のオリジンの不一致が判明し通信が失敗するとき
2.本リクエストを送ったもののオリジンの不一致がわかり、レスポンス受信後に通信が失敗するとき

![prerequest.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/926931/f12e2ac9-bc9f-bf6a-7334-05bedafdfbc8.png)
![response.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/926931/bf7ddd36-bca6-aea6-2f67-dab27ca36511.png)


##ここまでで押さえてほしいこと
 - CORSでは通信の可否がレスポンスヘッダで決まるということ
 - ブラウザからのリクエストにも数種類あり、それらもレスポンスヘッダにより通信の可否が決まってしまうということ
 
__要するにバックエンドのレスポンスヘッダの設定でCORS通信の可否が決まるということ！！__

今回CORS対策するのはこのレスポンスヘッダでオリジンの不一致を解消して本リクエストを成功させるものである。


#環境
Laravel Framework 8.35.1
ここから実際にLaravelでのCORS対策を紹介する。

なおフロントからのリクエストURLは全て `http://127.0.0.1:8000/api`である。番外編のみ `http://127.0.0.1:8000/` で送っている。

#1、`config/cors.php` を用いた方法

Laravel7.0以降から`config/cors.php`を用いたCORS対策が可能になったと聞く。一般的にLaravelの7.0以降バージョンを使っている場合は `cors.php`で書くのがスタンダードであるらしい。

```php:config/cors.php
<?php
return [
  // CORSヘッダーを出力するパスのパターン、任意でワイルドカード(*)が利用できる。
  //全てのルートを対象にする場合: ['*']
  //APIと特定の画像を対象にする例: ['api/*', 'resources/example.png']
 'paths' => ['*'],

  // マッチするHTTPメソッド。 `[*]` だと全てのリクエストにマッチする。
  //GETとPOSTだけを許可する場合: ['GET', 'POST']
 'allowed_methods' => ['*'],

  // 許可するリクエストオリジンの設定
  //`*`かオリジンに完全一致、またはワイルドカードが利用可。
 'allowed_origins' => ['*'],

  //正規表現によるオリジン指定。preg_matchの引数としてそのまま渡される。
 'allowed_origins_patterns' => [],

 // Access-Control-Allow-Headers response header　レスポンスヘッダーの指定
 'allowed_headers' => ['*'],

 //Access-Control-Expose-Headers レスポンスヘッダーの指定
 'exposed_headers' => false,

 //Access-Control-Max-Age レスポンスヘッダーの指定
 'max_age' => false,

 // Access-Control-Allow-Credentialsヘッダーを設定する。
 //falsy値を指定すると出力せず、truthyな値を渡せばtrueが出力される
 'supports_credentials' => false,
];
```
これでどんなURLからのアクセスもリクエストを許可するようになる。
ここでは `middleware` でも関わってくる重要な３つのレスポンスヘッダについて紹介する。

###Access-Control-Allow-Origin
`allowed_origins` は`Access-Control-Allow-Origin` のことであり、 通信を許可する送信元オリジンを指定し、送信元のオリジンがこれに該当するようであれば、クロスオリジンであってもブラウザは通信を許可する。

ちなみにローカルからのアクセスのみに限定する場合はこのように記述する。
`'allowed_origins' => ['http://localhost:8080']`

###Access-Control-Allow-Methods
`allowed_headers`  は `Access-Control-Allow-Methods` のことであり、許可するHTTPリクエストのメソッドを指定する。
Preflightリクエストの `Access-Control-Request-Method` で指定される値が設定されることになる。

###Access-Control-Allow-Headers
許可するHTTPヘッダーを指定する。
PreflightリクエストのAccess-Control-Request-Headersで指定される値が設定されることになる。
`Content-Type` の指定に関わる。

7.0より前のバージョンだとパッケージをインストールしなければならないらしいが、7.0以上のものは初期状態から備わっているらしい。
おそらく、そのため、このファイルの設定のみでCORSの対策ができる。

#2、`middleware` を用いた方法
次に、 `middleware` を用いてCORS対策を行ってみる。
先ほどもお伝えしたように7.0以降のバージョンを用いる場合はこの方法は適切ではない可能性がある。ただ自分自身の `middleware` への理解もかねてこの方法を利用してみた。
ここではもちろん、`config/cors.php`　がないとい前提である。

まずは、`middleware` の作成からである。

```
$ php artisan make:middleware Cors
```

作成した`middleware`を以下のように変更した。

```php:Cors.php
<?php

namespace App\Http\Middleware;

use Closure;

class Cors
{
    /**
     * Handle an incoming request.
     *
     * @param  \Illuminate\Http\Request  $request
     * @param  \Closure  $next
     * @return mixed
     */
    public function handle($request, Closure $next)
    {
        return $next($request)
            ->header('Access-Control-Allow-Origin', '*')
            ->header('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS')
            ->header('Access-Control-Allow-Headers', 'Content-Type');
    }
}
```
## `api.php`でのルーティング 
先ほどの記述で `middleware` の作成ができたので、 `middleware` を  `Karnel.php` に登録して、ルーティングに反映させる必要がある。
今回は `middleware` への理解もかねて３つの方法で `middleware` を登録してCORS対策をしてみた。

##グローバルミドルウェアへの登録
グローバルミドルウェアに登録することでルーティングに `middleware` を反映させなくても `Cors.php` を利用してCORS対策ができる。

```php:Karnel.php
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,
        'cors' => \App\Http\Middleware\Cors::class, // ここに追加
    ];
```
ルーティングの変更

```php:api.php
Route::get('/', [TestController::class, 'index']);
```
グローバルミドルウェアはルーティングに反映させなくても全てのルーティングにその `middleware` を適用するため、このような記述でCORS対策が可能である。

なお、この時にフロントからバックエンドを叩く時のリクエストURLは`http://127.0.0.1:8000/api`のように末尾に `api` をパラメータとして渡さないと `api.php` からルーティングを探さず、 `web.php` から探してしまうので注意が必要である。

##グループミドルウェアへの登録
次に、グループミドルウェアに登録してCORS対策を行う。Laravelでは標準的に `web`と`api`というグループミドルウェアがあり、`api.php` でのルーティングでは `api` のグループミドルウェアが適用されるので、今回はその `api`のなかに`cors`という `middleware` を登録してCORS対策を行う。

```php:Karnel.php
 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,
            'cors' => \App\Http\Middleware\Cors::class,// ここに追加
        ],
    ];
``` 
これでグループミドルウェアに登録できた。なお、今回は先ほどのルーティングに変更は必要ない。というのも先ほど述べたようにLaravelでは`api.php` でのルーティングでは `api` のグループミドルウェアが適用されるので、この `api` に `cors` 登録することで必然とCORS対策が可能となるのだ。

##ルートミドルウェアの利用
次にルートミドルウェアに登録して、`cors.php`を利用する。まずは、いつも通り `Karnel.php` への登録からだ。

```php:Karnel.php
  protected $routeMiddleware = [
        'auth' => \App\Http\Middleware\Authenticate::class,
        'auth.basic' => \Illuminate\Auth\Middleware\AuthenticateWithBasicAuth::class,
        'cache.headers' => \Illuminate\Http\Middleware\SetCacheHeaders::class,
        'can' => \Illuminate\Auth\Middleware\Authorize::class,
        'guest' => \App\Http\Middleware\RedirectIfAuthenticated::class,
        'password.confirm' => \Illuminate\Auth\Middleware\RequirePassword::class,
        'signed' => \Illuminate\Routing\Middleware\ValidateSignature::class,
        'throttle' => \Illuminate\Routing\Middleware\ThrottleRequests::class,
        'verified' => \Illuminate\Auth\Middleware\EnsureEmailIsVerified::class,
        'cors' => \App\Http\Middleware\Cors::class,//ここに追加
    ];
```
今回はルーティングにも `middleware` の利用も明示しなければならないのでルーティングにもこのように変更した。

```php:api.php
Route::group(['middleware' => ['cors']], function(){
    Route::options('/', function() {
        return response()->json();
    });
    Route::get('/', [TestController::class, 'index']);
});
```
また、このようなルーティングでもよい。

```php:api.php
Route::get('/', [TestController::class, 'index'])->middleware('cors');
```
個別に `middleware` の登録をしたときは後者のような書き方でも構わないが、共通の `middleware` を一括して適用する時は `Route:group` が便利である。

このように `middleware` への記述と登録、そして、ルーティングでCORS対策を行うことができる。

##番外編`web.php`でのルーティング
APIに関する通信は本来、 `api.php` でルーティングを行うべきであるが今回は実験として、`web.php` ではどのようにルーティングして動かすこのか挙動を確認した。
例えば、フロントのリクエストURLが `http://127.0.0.1:8000` のようなときもバックエンドと通信をしたいとすると、この時Laravelでは`web.php`でルーティングを探してしまうためにこの `web.php` に対してCORS対策をしてAPIを取得しなければならない。

この場合を検討すると主に二つの対策があると考えられる。最初に紹介した `config/cors.php` を記述する方法である。
この方法はルーティングも特に影響なく、CORS対策ができるから楽だと思われる。
次に後半に紹介した `middleware` を用いる方法である。この場合は紹介していようにCORS対策用の`middleware`を`Karnel.php` に登録してルーティングにも反映させなければならない。

例えば、ルートミドルウェアに登録した場合、`web.php` は以下のようなルーティングをして、`middleware` を利用する必要がある。

```php:web.php
Route::get('/', [MoviesController::class, 'get'])->middleware('cors');
```
ルーティングに関しては `Route:group` を利用しても良いが、いずれにしろ本来APIに関する通信は `api.php`に記述するようにAPIを作成する必要がある。

##まとめ
LaravelにおいてCORS対策は `config` で設定するのが楽ちん！　

##参考
https://qiita.com/hikkappi/items/1b51b9e58e8e391762de

https://larapet.hinaloe.net/2020/03/05/laravel-cors/

https://note.crohaco.net/2019/http-cors-preflight/

https://larapet.hinaloe.net/2020/03/05/laravel-cors/

https://developer.mozilla.org/ja/docs/Web/HTTP/CORS