LaravelでCORS対策の勉強をしていくつかの方法を知ったのでCORSへの理解と共にまとめる。
#そもそもCORSとは?
CORSとは Cross-Origin Resource Sharing
の略で日本語だとオリジン間リソース共有 (CORS)と言われる。
同一オリジンポリシーを超えて通信を行うことをクロスオリジン間通信といい、その中でリソースの共有、つまりデータを共有することをCORSという。
###オリジンとは何か?
ではオリジンとは何かというと、スキーム + ホスト + ポート番号で構成される組み合わせのことである。
この組み合わせが一致すると同一オリジンと見なされて、一つでも一致しないとクロスオリジンとみなされる。
ドメインと似ているが、プロトコルとポート番号を含んでいる点がドメインとは異なる。
アクセス元のオリジンが同一オリジンの場合、レスポンスヘッダの有無や内容にかかわらず通信は成功する。
しかし、クロスオリジン間通信では同一オリジンポリシーという制約が発生する。この設定を調整して、__異なるオリジンへアクセスできるように CORSを設定することをCORS対策__という。
レスポンスを受けるサーバー側ではリクエストを受ける下記レスポンスヘッダによってクロスオリジン通信の可否が左右される。
- Access-Control-Allow-Origin
- Access-Control-Allow-Credentials
- Access-Control-Expose-Headers
- Access-Control-Max-Age
- Access-Control-Allow-Methods
- Access-Control-Allow-Headers
##シンプルリクエストとプリフライトリクエスト
このクロスオリジン間通信ではシンプルリクエストとプリフライトリクエストというブラウザからのリクエストが飛ばされ、これらがあることでクロスオリジン間通信、つまりブラウザはサーバーからレスポンスを得ることができる。
###シンプルリクエスト
シンプルリクエストであるかどうかは以下の__全ての条件__に当てはまるかどうかである。
シンプルリクエストの時はプリフライトリクエストを引き起こさず、外部との通信が可能である。
-
リクエストのメソッドが GET, POST, HEAD のいずれかに設定されている場合
-
Content-Type ヘッダーが以下の設定である時
-
application / x-www-form-urlencoded
-
multipart / form-data
-
text / plain
-
特定のHTTPリクエストヘッダが含まれない (詳細はこちら→simple_requests)
-
Accept
-
Accept-Language
-
Content-Language
-
DPR
-
Downlink
-
Save-Data
-
Viewport-Width
-
Width
###プリフライトリクエスト
プリフライトリクエストはシンプルリクエストではない時に出るリクエストである。その本リクエストを送信しても安全かどうかを確かめるいわゆる毒味のようなリクエストであり、OPTIONS メソッドを用いた通信で安全かどうかの確認をする。
###通信が失敗するとき・・・
クロスオリジン通信ではレスポンスヘッダの オリジン とリクエスト送信元オリジンが一致しないとブラウザは通信を失敗する。要するに同一オリジンポリシーに反しているということである。この時の多くのパターンとして2パターンある。
1.プリフライトリクエスト(本リクエスト前)により相互のオリジンの不一致が判明し通信が失敗するとき
2.本リクエストを送ったもののオリジンの不一致がわかり、レスポンス受信後に通信が失敗するとき
##ここまでで押さえてほしいこと
- 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
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
でも関わってくる重要な3つのレスポンスヘッダについて紹介する。
###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
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
への理解もかねて3つの方法で middleware
を登録してCORS対策をしてみた。
##グローバルミドルウェアへの登録
グローバルミドルウェアに登録することでルーティングに middleware
を反映させなくても Cors.php
を利用してCORS対策ができる。
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, // ここに追加
];
ルーティングの変更
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対策を行う。
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
への登録からだ。
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
の利用も明示しなければならないのでルーティングにもこのように変更した。
Route::group(['middleware' => ['cors']], function(){
Route::options('/', function() {
return response()->json();
});
Route::get('/', [TestController::class, 'index']);
});
また、このようなルーティングでもよい。
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
を利用する必要がある。
Route::get('/', [MoviesController::class, 'get'])->middleware('cors');
ルーティングに関しては Route:group
を利用しても良いが、いずれにしろ本来APIに関する通信は api.php
に記述するようにAPIを作成する必要がある。
##まとめ
LaravelにおいてCORS対策は config
で設定するのが楽ちん!
##参考
https://qiita.com/hikkappi/items/1b51b9e58e8e391762de