【Laravel】Laravel SanctumでAPI認証を実装する

laravelアイキャッチ Laravel

こんにちは、かつコーチです。

前回はBreeze・Jetstream・Fortify・Sanctumの違いを整理して、「どの認証機能を選べばいいか」の判断軸を紹介しました。

今回はその中でも、SPA連携やモバイルアプリのバックエンドでよく使われるSanctumを使って、実際にAPI認証を実装していきます。

セッション認証とは仕組みがかなり違うので、「なぜトークンが必要なのか」から順を追って説明します。

Sanctumとは?API認証の基本

Sanctumが解決する課題

Sanctumは、SPA(Single Page Application)・モバイルアプリ・単純なAPIトークンによる認証を、軽量に実現するためのLaravel公式パッケージです。

通常のWebアプリでは、ログインするとサーバーがセッション(ログイン状態をサーバー側で保持する仕組み)を発行し、ブラウザはCookieでそのセッションIDをやり取りします。

ですが、フロントエンドとバックエンドが別ドメイン・別プロジェクトになるケースや、モバイルアプリからAPIを叩くケースでは、Cookieベースのセッション認証だけでは対応しづらい場面が出てきます。

  • スマホアプリにはブラウザのCookie機構がない
  • 別ドメインのSPAからAPIを呼ぶとCookieの扱いが複雑になる
  • 単純に「このリクエストは誰からのものか」をトークンで判定したい

こうしたケースに対応するために、SanctumはAPIトークン認証SPA向けのCookieベース認証の両方を1つのパッケージでサポートしています。

トークン認証とセッション認証の違い

両者の違いを整理すると、次のようになります。

セッション認証トークン認証(Sanctum)
認証情報の保存場所サーバー側のセッションストアデータベースのトークンテーブル
クライアントが送るものセッションCookieAuthorization: Bearer {token} ヘッダー
主な用途通常のWebアプリ(Blade)モバイルアプリ・外部API連携
ドメインをまたぐ利用苦手得意

私が最初にAPI開発を任されたとき、「ログインしてるのにAPIが401(未認証)を返す」という現象で丸一日悩んだことがあります。

原因は単純で、フロントは通常のセッションCookieしか送っていないのに、APIエンドポイント側はAuthorizationヘッダーのトークンを期待していた、という認証方式のミスマッチでした。

「どの認証方式を使っているか」を最初にきちんと確認する癖をつけておくと、こういう事故を防げます。

Sanctumのセットアップと基本実装

インストールとマイグレーション

まずはSanctumをインストールします(Laravel 11以降は初期状態で組み込まれていることが多いですが、手動導入する場合の手順です)。

composer require laravel/sanctum

php artisan vendor:publish --provider="Laravel\Sanctum\SanctumServiceProvider"

php artisan migrate

マイグレーションを実行すると、personal_access_tokens というテーブルが作成されます。

ここに、発行したAPIトークンのハッシュ値や有効期限などが保存されます。

Userモデルにトレイトを追加する

トークンを発行できるようにするため、UserモデルにHasApiTokensトレイトを追加します。

<?php
// app/Models/User.php

namespace App\Models;

use Illuminate\Foundation\Auth\User as Authenticatable;
use Laravel\Sanctum\HasApiTokens;

class User extends Authenticatable
{
    use HasApiTokens;

    // 以下、既存のプロパティ・メソッド
}

HasApiTokensを使うことで、$user->createToken()のようなメソッドが使えるようになります。

ログインしてトークンを発行する

APIログイン用のエンドポイントを作り、認証成功時にトークンを発行します。

<?php
// routes/api.php

use App\Http\Controllers\Api\AuthController;

Route::post('/login', [AuthController::class, 'login']);
<?php
// app/Http/Controllers/Api/AuthController.php

namespace App\Http\Controllers\Api;

use App\Http\Controllers\Controller;
use App\Models\User;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Hash;
use Illuminate\Validation\ValidationException;

class AuthController extends Controller
{
    public function login(Request $request)
    {
        $request->validate([
            'email' => ['required', 'email'],
            'password' => ['required'],
        ]);

        $user = User::where('email', $request->email)->first();

        if (! $user || ! Hash::check($request->password, $user->password)) {
            throw ValidationException::withMessages([
                'email' => ['メールアドレスまたはパスワードが正しくありません。'],
            ]);
        }

        // "api-token"はこのトークンに付ける名前(管理用のラベル)
        $token = $user->createToken('api-token')->plainTextToken;

        return response()->json([
            'user' => $user,
            'token' => $token,
        ]);
    }
}

createToken()が返すplainTextTokenは、発行された瞬間しか平文で取得できません

データベースにはハッシュ化された値しか保存されないため、レスポンスで返したトークンをクライアント側で必ず保存しておく必要があります。

トークンを使ってAPIにアクセスする

発行したトークンは、リクエストヘッダーに付けて送ります。

curl -X GET https://example.com/api/user \
  -H "Authorization: Bearer 1|abcdefghijklmnopqrstuvwxyz"

このヘッダーを受け取るAPI側は、auth:sanctumミドルウェアで認証済みユーザーかどうかをチェックします。

<?php
// routes/api.php

Route::middleware('auth:sanctum')->get('/user', function (Request $request) {
    return $request->user();
});

auth:sanctumを通過すると、$request->user()でトークンの持ち主のUserインスタンスを取得できます。

つまずきやすいポイント:ミドルウェアの付け忘れ

保護すべきルートに認証ミドルウェアを忘れる

Sanctumを導入していても、ミドルウェアを付け忘れると誰でもアクセスできてしまいます

これは私が実際にやってしまったミスで、動作確認中に一時的にミドルウェアを外したまま、そのままコミットしてしまったことがありました。

❌ Before:認証ミドルウェアが付いていない

<?php
// routes/api.php

// 誰でもアクセスできてしまう危険な状態
Route::get('/orders', [OrderController::class, 'index']);
Route::post('/orders', [OrderController::class, 'store']);

このままだと、トークンなしでも/ordersにアクセスでき、ログイン不要で注文情報が見えてしまいます。

✅ After:auth:sanctumミドルウェアでグループ化する

<?php
// routes/api.php

Route::middleware('auth:sanctum')->group(function () {
    Route::get('/orders', [OrderController::class, 'index']);
    Route::post('/orders', [OrderController::class, 'store']);
});

複数のルートをまとめて保護したい場合は、group()で囲んでおくと付け忘れを防げます。

個別に->middleware('auth:sanctum')を付けるより、グループ化しておくほうが「保護されたエリア」であることが目視でも分かりやすくなります。

トークンの権限(アビリティ)を絞る

トークンにはアビリティ(そのトークンでできる操作の範囲)を設定できます。

読み取り専用のトークンを発行したい場合は、次のように指定します。

<?php
// 読み取り専用のアビリティだけを持つトークンを発行する
$token = $user->createToken('read-only-token', ['orders:read'])->plainTextToken;

コントローラ側では、そのアビリティを持っているかをチェックできます。

<?php
// app/Http/Controllers/Api/OrderController.php

public function store(Request $request)
{
    if (! $request->user()->tokenCan('orders:write')) {
        abort(403, '書き込み権限がありません。');
    }

    // 注文作成処理
}

「このトークンは閲覧専用アプリ用」「こちらは管理画面用」のように用途を分けたい場合、アビリティで細かく権限を制御できます。

応用:SPA向けのCookieベース認証

モバイル・外部API向けとSPA向けの使い分け

Sanctumには、ここまで紹介したトークン認証のほかに、SPA向けのステートフル認証というモードもあります。

同一ドメイン(またはサブドメイン)で動くSPAの場合、トークンを発行せずにCookieベースでセッション認証と同じ仕組みを使えます。

config/sanctum.phpstatefulにフロントエンドのドメインを登録し、EnsureFrontendRequestsAreStatefulミドルウェアを通すことで有効になります。

<?php
// config/sanctum.php(抜粋)

'stateful' => explode(',', env(
    'SANCTUM_STATEFUL_DOMAINS',
    'localhost,localhost:3000,127.0.0.1'
)),

判断軸としては、次のように考えるとシンプルです。

  • 同一ドメインのSPA(React/Vueなど)→ Cookieベースのステートフル認証
  • モバイルアプリ・別ドメインの外部連携 → トークン認証

どちらのモードでも、auth:sanctumミドルウェア自体は共通で使えるのがSanctumの便利なところです。

まとめ

この記事のポイント

  • SanctumはAPIトークン認証とSPA向けCookie認証の両方をサポートする軽量パッケージ
  • HasApiTokensトレイトとcreateToken()でトークンを発行し、Authorization: Bearerヘッダーで送る
  • 保護したいAPIルートにはauth:sanctumミドルウェアを忘れずグループ化して付ける
  • アビリティを使うと、トークンごとに操作範囲を絞り込める
  • 同一ドメインのSPAならCookieベースのステートフル認証も選択肢になる

次に読むべき記事

API認証ができるようになったら、次は「誰がどのデータを操作できるか」という認可の仕組みを見ていきましょう。

→ 次の記事:Policy・Gateで認可(権限チェック)を実装する

タイトルとURLをコピーしました