Laravel SanctumでSPA向けAPI認証を実装したら、セッション管理がかなりスッキリした話
LaravelでReact SPAのバックエンドAPIを作る際の認証にSanctumを使ったところ、Passportと比べてセットアップがかなり軽く、CSRFトークンの扱いも含めてスムーズに実装できました。
🙌 結論から
Laravel Sanctum を使ってSPAのAPI認証を実装したところ、Passportと比べてセットアップがかなり軽く、Cookieベースの認証がシンプルに実現できました!
SPAとAPIが同一ドメインの場合はCSRFトークンを使ったCookieセッション方式が使えて、JWTをlocalStorageで自前管理しなくてよいのが本当に助かります。
モバイルアプリや外部クライアントにはAPIトークン方式も使えるので、1つのパッケージで複数の認証ニーズに対応できます(^_^)
💡 Sanctum とはなにか
Laravel Sanctum は、SPA・モバイルアプリ・シンプルなAPIトークン認証のための公式パッケージです。
同じ Laravel エコシステムの Passport はOAuth2の完全実装を提供しますが、SPAやシンプルなAPIには機能過多になりがちです。
Sanctum はその「軽量版」という位置づけで、セットアップがかなりシンプルなのが特徴です。
Laravel 10以降はデフォルトで含まれているケースも多く、追加インストール不要なプロジェクトもあります。
👀 どんな場面で使うか
Sanctum には2つの認証方式があります。
- Cookie/セッション方式: SPAとAPIが同一ドメインにある場合に使う方法
- APIトークン方式: モバイルアプリや別ドメインのクライアント向け
今回は社内ツール的なSPAで使いました。
バックエンドは Laravel、フロントエンドは React で、同一ドメインにデプロイする構成です。
この場合はCookieセッション方式が使えるので、JWTを自分で発行・管理する必要がありません!
💪 実装の流れ
まずインストールと設定です:
composer require laravel/sanctum
php artisan vendor:publish --provider="Laravel\Sanctum\SanctumServiceProvider"
php artisan migrate
app/Http/Kernel.php(または bootstrap/app.php)の api ミドルウェアに EnsureFrontendRequestsAreStateful を追加します:
'api' => [
\Laravel\Sanctum\Http\Middleware\EnsureFrontendRequestsAreStateful::class,
'throttle:api',
\Illuminate\Routing\Middleware\SubstituteBindings::class,
],
ログインのAPIエンドポイントはこんな感じです:
Route::post('/login', function (Request $request) {
if (!Auth::attempt($request->only('email', 'password'))) {
return response()->json(['message' => 'Unauthorized'], 401);
}
$request->session()->regenerate();
return response()->json(Auth::user());
});
フロントエンド(React)からは、まずCSRFクッキーを取得してからログインします:
await axios.get('/sanctum/csrf-cookie');
await axios.post('/api/login', { email, password });
/sanctum/csrf-cookie へのリクエストが XSRF-TOKEN クッキーをセットしてくれるので、その後のリクエストは自動的にCSRF保護が適用されます。
✅ 使ってみた感想
JWTを自前管理しなくて済む
Cookieセッション方式だとブラウザがCookieを自動で扱ってくれるので、JWTをlocalStorageに保存するセキュリティリスクを気にしなくてよくなります!
以前 localStorage に JWT を入れていた実装を見直すきっかけにもなりました(^^)
XSSでlocalStorageを抜かれるリスクを減らせるのは、実際に仕事で使ってみて安心感が大きかったです。
ルート保護がシンプル
認証が必要なルートには auth:sanctum ミドルウェアを付けるだけです:
Route::middleware('auth:sanctum')->group(function () {
Route::get('/user', fn(Request $request) => $request->user());
Route::apiResource('posts', PostController::class);
});
未認証のリクエストは自動的に401を返してくれます。
Passport だと auth:api ミドルウェアやスコープの設定が必要でしたが、Sanctum はかなりシンプルです。
🤔 注意したいポイント
CORSの設定を config/cors.php で正しく設定しないと、Cookieが送られないケースがあります(・_・;)
特に supports_credentials を true にするのを忘れがちです:
// config/cors.php
'supports_credentials' => true,
'allowed_origins' => ['http://localhost:3000'],
フロント側も axios.defaults.withCredentials = true を設定しておく必要があります。
あと .env の SESSION_DOMAIN と SANCTUM_STATEFUL_DOMAINS にフロントエンドのドメインを追加しておくことも重要です。
CORSまわりでハマる人がかなり多い印象なので、最初にまとめて確認しておくのがかなりオススメ!
🔑 abilitiesでトークンの権限を絞る
Cookieセッション方式のSPA認証は一段落したんですが、その後モバイルアプリからも同じAPIを叩きたいという要望が来て、APIトークン方式も併用することになりました。
このとき知って意外と助かったのが、**トークンに「できること」を制限できる abilities(OAuthでいうスコープに近いもの)**です。
// 読み取り専用のトークンを発行する
$token = $user->createToken('mobile-readonly', ['posts:read'])->plainTextToken;
// 投稿・削除もできるトークンを発行する
$token = $user->createToken('mobile-full', ['posts:read', 'posts:write', 'posts:delete'])->plainTextToken;
コントローラ側では tokenCan() で、そのリクエストのトークンが特定の操作を許されているか確認できます。
Route::delete('/posts/{post}', function (Request $request, Post $post) {
if (! $request->user()->tokenCan('posts:delete')) {
abort(403);
}
$post->delete();
return response()->noContent();
})->middleware('auth:sanctum');
私はこれを使って、**「閲覧専用の外部連携用トークン」と「フル機能のモバイルアプリ用トークン」**を明確に分けるようにしました。
以前は「このAPIキーが漏れたら何でもできてしまう」という不安が常にあったんですが、abilitiesで権限を絞ってから、トークンごとに被害範囲を限定できる安心感がかなり大きくなりました(・∀・)
Cookie方式のSPA認証だけを使っているうちは気づきにくい機能ですが、外部連携やモバイルアプリを後から足すことになったら、真っ先に検討する価値があると思います。
パパ系Webエンジニア〜愛用ガジェット一覧 🛍️
この記事を書いている作業環境のガジェットを楽天ROOMにまとめています ✨ 在宅エンジニアのデスク作りの参考にどうぞ 🙌
🙌 まとめ
Laravel Sanctum を使ったSPA認証は、Passportほどの設定量がなく、シンプルに実装できました!
まとめると:
- 同一ドメインのSPAにはCookieセッション方式が最適
- JWTのlocalStorage管理が不要でセキュリティ的にも安心
- CORS の
supports_credentials: trueだけは忘れずに設定する
Laravel でAPIバックエンドを立てる機会があれば、まず Sanctum を試してみるのがかなりオススメです。
シンプルな認証ならこれで十分で、Passportの複雑な設定は不要です(^o^)/
※ Laravel・Sanctumのバージョンによって仕様が変わることがあります。最新情報は Laravel公式ドキュメント をご確認ください👀