よくよく考えれば当たり前のことだったんですが、若干ハマってしまったので・・・
同様にハマった人の助けになれば幸いです。
普通に CORS 対応をすすめる
Access-Control-Allow-Origin などをレスポンスヘッダーに含ませる Middleware の作成
いろいろな人が記事をまとめてくれているので、ググればすぐ見つかると思います。
各ヘッダーの意味はこちらを参照。
Access-Control-Allow-Origin
Access-Control-Allow-Methods
Access-Control-Allow-Headers
<?php
namespace App\Http\Middleware;
use Closure;
class Cors
{
public function handle($request, Closure $next)
{
return $next($request)
->header('Access-Control-Allow-Origin', '*')
->header('Access-Control-Allow-Methods', 'GET, POST')
->header('Access-Control-Allow-Headers', 'Accept, X-Requested-With, Origin, Content-Type');
}
}
api Middleware グループに追加します。
これで api 全体に適用されるようになります。(もちろんルーティング定義の際に api Middleware グループを指定している場合に限ります)
<?php
namespace App\Http;
use Illuminate\Foundation\Http\Kernel as HttpKernel;
class Kernel extends HttpKernel
{
:
/**
* The application's route middleware groups.
*
* @var array
*/
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:60,1',
\Illuminate\Routing\Middleware\SubstituteBindings::class,
\App\Http\Middleware\Cors::class, // ★追加
],
];
:
}
大抵はこれで十分なんですが、 プリフライトリクエストが発生する場面ではこれだけでは不十分でした。
プリフライトリクエストについてはこちら
https://developer.mozilla.org/ja/docs/Web/HTTP/CORS#Simple_requests
https://developer.mozilla.org/ja/docs/Web/HTTP/CORS#Preflighted_requests
プリフライトリクエストの場合は api Middleware が使用されない
どうやらプリフライトリクエストについてのルーティング定義は自動的に Laravel が作成するようです。
その場合は、Middleware の適用などが行われません。
つまり
こんなルーティング定義を用意しておきます。
Route::get('sample', function () { echo 'sample api'; });
URL や Middleware の適用はこんな状態です。
http://domain/api/sample にアクセスすると api Middleware グループが適用されます。
$ php artisan route:list | grep 'sample'
| | GET|HEAD | api/sample | front.api. | Closure | api |
この状態で CORS となるようにブラウザの Console 上でこの API を実行してみます。
fetch('https://dummy.jp/api/sample', {method: 'GET', headers: {'X-Requested-With': 'XMLHttpRequest'}})
.then(response => console.log(response))
.catch(response => console.error(response))
// -->
// Access to fetch at 'https://dummy.jp/api/sample' from origin 'https://developer.mozilla.org' 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. If an opaque response serves your needs, set the request's mode to 'no-cors' to fetch the resource with CORS disabled.
先程追加した Cors Middleware で設定しているはずの Access-Control-Allow-Origin が効いていません。
ブラウザの開発者ツールからこのときにリクエストの情報を見てみると
- プリフライトリクエストとして OPTIONS のリクエストが最初に飛ぶ
- その時のレスポンスヘッダーに Access-Control-Allow-Origin は含まれていない
である事がわかります。
OPTIONS に対応するルーティング定義は用意していない
php artisan route:list の結果からわかるように OPTIONS に対するルーティング定義は用意していません。
Laravel は対応していない HTTP リクエストメソッドに対しては 405 エラーを返しますが、そのエラーは発生していません。
このことから OPTIONS の場合は Laravel がプリフライトリクエストだからいい感じにレスポンスしてくれてると思われます。
ソースコードを grep してみたんですが、 OPTIONS ではいろいろな箇所がヒットして特定しきれなかったので諦めました。
Laravel がいい感じにレスポンスしてくれるときは Middleware が効かない
URL 自体はルーティング定義にある api/sample なので、 api Middleware グループを適用してほしいところですが、そこまではやってくれないようです。
試しに App\Http\Middleware\Cors に全然関係ない echo や exit を埋め込んでも動作に変化はありませんでした。
なので、レスポンスヘッダーに Access-Control-Allow-Origin が含まれることはありません。
対応方法
もうちょっとスマートな方法もあるきもしますが、ひとまず以下の方法で問題は解消できました。
- すべてのルーティングに Cors Middleware を適用する
- Cors Middleware 内で API Middleware グループを適用しようとしている URL であれば Access-Control-Allow-Origin をレスポンスヘッダーに含める
としました。
すべてのルーティングに Cors Middleware を適用する
<?php
namespace App\Http;
use Illuminate\Foundation\Http\Kernel as HttpKernel;
class Kernel extends HttpKernel
{
:
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,
\App\Http\Middleware\Cors::class, // ★追加
];
:
}
Cors Middleware 内で API Middleware グループを適用しようとしている URL であれば Access-Control-Allow-Origin をレスポンスヘッダーに含める
適用されている Middleware グループを取得できれば一番良かったんですが、どうにもうまく取れなかったので URL の一部から API であるかを判定するようにしています。
判定方法は実際の実装に合わせて柔軟に変更してください。
<?php
namespace App\Http\Middleware;
use Closure;
class Cors
{
public function handle($request, Closure $next)
{
// すべてのレスポンスに CORS 用のヘッダーを追加する必要はないので URL から判断する
$paths = explode('/', \Request::getPathInfo());
if ($paths[1] === 'api') {
return $next($request)
->header('Access-Control-Allow-Origin', '*')
->header('Access-Control-Allow-Methods', 'GET, POST')
->header('Access-Control-Allow-Headers', 'Accept, X-Requested-With, Origin, Content-Type');
}
return $next($request);
}
}
再度 CORS となるようにブラウザの Console 上でこの API を実行してみる
今度はエラーが発生せず、正常に通信できるようになります。