【Laravel】Sanctumで認証API開発方法!ログイン/ログアウトとトークン管理

【Laravel】Sanctumで認証API開発方法!ログイン/ログアウトとトークン管理

この記事からわかること

  • Laravel認証API開発する流れ
  • Sanctum導入使い方
  • auth.phpsanctum.phpとは?
  • 未認証時のハンドリング方法

index

[open]

\ アプリをリリースしました /

みんなの誕生日

友達や家族の誕生日をメモ!通知も届く-みんなの誕生日-

posted withアプリーチ

環境

Laravel Sanctumとは?

公式リファレンス:Sanctum

Laravel Sanctum」とはLaravelにおける認証システムの1つでトークンベースでの認証APIを開発することが可能になっています。主にSPA(シングルページアプリケーション)やモバイルアプリケーションと連携して認証機能を実装するのに活用することが可能です。

APIトークン

SanctumはAPIトークンを発行してユーザー認証を行うシンプルな設計です。ユーザーのログイン時などにトークンを発行しそのトークンを使用して認証を行い不正なアクセスでないかを検知します。有効期限は年単位で設定できるようですがユーザーのタイミングでトークンをを無効にすることも可能になっています。

APIトークンをデータベーステーブルに保存し、有効なAPIトークンを含む必要があるAuthorizationヘッダを介して受信し、HTTPリクエストを認証するといった仕組みになっています。

導入手順

最新のLaravelにはデフォルトでSanctumが導入されています。私の動作環境ではLaravel:8.83.29で導入済みであることを確認できました。確認方法はcomposer.jsonファイル内にlaravel/sanctumを含んでるかどうかで確認できます。

もし未導入の場合は以下のコマンドを実行します。

$ composer require laravel/sanctum

次に以下のコマンドを実行してSanctum設定ファイルとマイグレーションファイルをリソース公開しておきます。

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

ここでAPIトークンを格納するためのデータベーステーブルを生成するためにmigrateを実行します。

$ php artisan migrate

最後にapp/Http/Kernel.phpファイル内のapiミドルウェアグループに追加して導入は完了です。

'api' => [
    \Laravel\Sanctum\Http\Middleware\EnsureFrontendRequestsAreStateful::class,
    'throttle:api',
    \Illuminate\Routing\Middleware\SubstituteBindings::class,
],

APIの開発実装手順

認証APIを開発する上で必要になるクラスなどを先にまとめておきます。

  1. Userモデル
  2. 認証ハンドリングを行うController
  3. ルーティング(api.php)

Userモデル

Laravel Sanctumがデフォルトで入っているおかげかUserモデルもデフォルトで用意されています。HasApiTokensトレイトが記述してあればOKです。

class User extends Authenticatable
{
    use HasApiTokens, HasFactory, Notifiable;

    protected $fillable = [
        'name',
        'email',
        'password',
    ];

    protected $hidden = [
        'password',
        'remember_token',
    ];

    protected $casts = [
        'email_verified_at' => 'datetime',
    ];
}

認証ハンドリングを行うController

続いて認証ハンドリングを行うためのControllerを作成します。

$ php artisan make:controller AuthController

作成できたら「新規登録」、「ログイン」、「ログアウト」、「User情報取得」の4つのメソッドを実装していきます。Userモデルを使用してそのままDBへのCRUD処理が実装できるので楽に実装可能です。トークンを取得したい場合はcreateToken('auth_token')->plainTextTokenでAPIトークンを取得することが可能です。

class AuthController extends Controller
{
    // 新規登録
    public function register(Request $request)
    {
        $validatedData = $request->validate([
            'name' => 'required|string|max:255',
            'email' => 'required|email|unique:users',
            'password' => 'required|string|min:8|confirmed',
        ]);

        $user = User::create([
            'name' => $validatedData['name'],
            'email' => $validatedData['email'],
            'password' => Hash::make($validatedData['password']),
        ]);

        return response()->json(['message' => 'User registered successfully'], 201);
    }

    // ログイン
    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' => ['The provided credentials are incorrect.'],
            ]);
        }

        $token = $user->createToken('auth_token')->plainTextToken;

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

    // ログアウト
    public function logout(Request $request)
    {
        $request->user()->tokens()->delete();

        return response()->json(['message' => 'Logged out successfully']);
    }

    // 認証ユーザー情報
    public function user(Request $request)
    {
        return response()->json($request->user());
    }
}

またトークンを明示的に破棄するには$request->user()->tokens()->delete()を使用します。

ルーティング(api.php)

最後にルーディングです。APIなのでapi.phpに記述します。Sanctumを使用して認証を施したいルートに対してRoute::middleware('auth:sanctum')を付与します。以下の場合ユーザー情報取得とログアウト処理にのみ認証が必須になります。


Route::post('register', [AuthController::class, 'register']);
Route::post('login', [AuthController::class, 'login']);
Route::middleware('auth:sanctum')->get('user', [AuthController::class, 'user']);
Route::middleware('auth:sanctum')->post('logout', [AuthController::class, 'logout']);

APIの疎通テスト

これでAPIの開発が完了しました。実際に正常に動作するか確認していきます。今回は「Postman」を使用して疎通テストしていきます。

新規登録

データベースが空の状態なので最初に新規登録を行います。新規登録はPOSTメソッドでhttp://localhost/oiwai/api/registerにアクセスします。その際にbodyJSON形式でユーザー登録情報を渡します
oiwaiは私の環境構造上入っているだけなので抜いてください。

{
    "name": "テスト太郎",
    "email": "test@example.com",
    "password": "password123",
    "password_confirmation": "password123"
}

Sendボタンで実行するとデータベースにユーザーが登録されます。

Laravel】Sanctumで認証API開発方法!ログイン/ログアウトとトークン管理

ログイン

続いてログインしたいのでPOSTメソッドでhttp://localhost/oiwai/api/loginにアクセスします。こちらもbodyJSON形式でユーザーログイン情報を渡します

{
    "email": "test@example.com",
    "password": "password123"
}

するとレスポンスでAPIトークンを返すように設定していたのでトークンを取得できます。

{
    "access_token": "4|Jr8DaKo6VTSg2WhLB63wyWbRGWsf04MIPb5UMvIl",
    "token_type": "Bearer"
}
Laravel】Sanctumで認証API開発方法!ログイン/ログアウトとトークン管理

認証してアクセスする

最後に認証が必要なルートへアクセスする方法です。GETメソッドでhttp://localhost/oiwai/api/userにアクセスします。先ほど取得したトークンはAuthorizationヘッダーに渡します。Postmanの場合は以下のようにBearer Tokenを指定して、該当のトークンを記述します。

しかしなぜかサーバーエラー:500になってしまいました。500になっている原因は未認証でリダイレクトが走り、表示されているエラー:Route [login] not defined.loginViewが未定義なので正しいのですが、トークンを正しく指定しても認証に失敗してしまいました。解決方法は模索中です・・・

Laravel】Sanctumで認証API開発方法!ログイン/ログアウトとトークン管理

※追記:2025/1/25 認証ルートの疎通ですがPostmanだと成功せず、php artisan serveでサーバーを立ち上げてテストした場合は正常に動作しました。環境上oiwaiが入っていたりするのが良くなかったのかもしれません。

// Laravelサーバー立ち上げ
$ php artisan serve

// 新規登録
$ curl -X POST http://127.0.0.1:8000/api/register \
-H "Content-Type: application/json" \
-d '{
  "name": "testuser",
  "email": "testuser@example.com",
  "password": "password123",
  "password_confirmation": "password123"
}'
// Response
{"message":"User registered successfully"}    

// ログイン
$ curl -X POST http://127.0.0.1:8000/api/login \
-H "Content-Type: application/json" \
-d '{
  "email": "testuser@example.com",
  "password": "password123"
}'
// Response
{"access_token":"5|TNbh9DywCZRfQBCAiI6NjqCxFld8D77HPeAM68ot","token_type":"Bearer"}

// 認証ルート(ユーザー情報取得)
$ curl -X GET http://127.0.0.1:8000/api/user \
-H "Authorization: Bearer 5|TNbh9DywCZRfQBCAiI6NjqCxFld8D77HPeAM68ot" \
-H "Content-Type: application/json"
// Response
{"id":2,"name":"testuser","email":"testuser@example.com","email_verified_at":null,"created_at":"2025-01-24T14:52:30.000000Z","updated_at":"2025-01-24T14:52:30.000000Z"}

※追記:2025/2/2 ちなみにhttp://localhost/認証失敗していた原因はURLのpublicを消すために.htaccessを設置したことでした。

追加した際に以下を入れておかないとAuthorization ヘッダーをリダイレクト先に回せず抜け落ちてしまっていたみたいですね・・・


# Authorization ヘッダーを環境変数に保持
RewriteCond %{HTTP:Authorization} ^(.*)
RewriteRule .* - [E=HTTP_AUTHORIZATION:%1]

Authorizationヘッダーに渡す値

ヘッダーにはAuthorizationKeyとして値にトークンを渡します。トークンはBearer トークン形式で渡さないと正常に認証できないので注意してください。

またレスポンスで取得したトークンは接頭辞に5|が付与されています。これはデータベースのトークンIDで5|を付与しなくても認証は通ります。

5|TNbh9DywCZRfQBCAiI6NjqCxFld8D77HPeAM68ot

またデータベースに格納されているトークンはハッシュ化されたものになるので認証で使用するのは$user->createToken('auth_token')->plainTextTokenで取得した生のトークンを使用してください。

未認証時のハンドリング

認証に失敗した際にどのようなハンドリングが行われているのか調べてみました。まずloginViewへリダイレクトしていたのはApp/Http.Middleware/Authenticate.phpでした。


class Authenticate extends Middleware
{
    // ユーザーが認証されていない場合にリダイレクトされるパスを取得します。
    protected function redirectTo($request)
    {
        if (! $request->expectsJson()) {
            return route('login');
        }
    }
}

そしてredirectTo自体はvendor/laravel/framework/src/Illuminate/Foundation/Exceptions/Handler.phpunauthenticatedメソッドから呼び出されているぽいですね。


protected function unauthenticated($request, AuthenticationException $exception)
{
    return $this->shouldReturnJson($request, $exception)
                ? response()->json(['message' => $exception->getMessage()], 401)
                : redirect()->guest($exception->redirectTo() ?? route('login'));
}

vendorの中ではなくapp/Exception/Handler.phpにも同名のものが存在しています。ここでunauthenticatedをオーバーライドすれば任意のハンドリングが行えるかもです。


// 認証失敗時のレスポンスをカスタマイズ
protected function unauthenticated($request, AuthenticationException $exception)
{
    return response()->json([
        'message' => '認証に失敗しました。トークンが無効または期限切れです。',
    ], 401);
}

Sanctumの設定ファイル

Sanctumの設定はconfig/sanctum.phpで管理されています。

Cookieベースの認証をどのドメインで許可するか

'stateful' => explode(',', env('SANCTUM_STATEFUL_DOMAINS', sprintf(
    '%s%s',
    'localhost,localhost:3000,127.0.0.1,127.0.0.1:8000,::1',
    env('APP_URL') ? ','.parse_url(env('APP_URL'), PHP_URL_HOST) : ''
))),

トークンの有効期限

nullは無制限。有効期限を設定したい場合は、分単位で数値を指定。(例: 60 で1時間)。

'expiration' => null,

Middleware Sanctumの認証で使用するミドルウェアを指定

'middleware' => [
    'verify_csrf_token' => App\Http\Middleware\VerifyCsrfToken::class,
    'encrypt_cookies' => App\Http\Middleware\EncryptCookies::class,
],

APIトークン認証のガード

'guard' => ['web'],

Laravel自体の認証設定ファイル

Laravel自体の認証設定config/auth.phpで管理されています。

デフォルトで使用する認証ガード

'defaults' => [
    'guard' => 'sanctum',
    'passwords' => 'users',
],

認証の仕組みを設定

'guards' => [
    'web' => [
        'driver' => 'session',
        'provider' => 'users',
    ],
    'sanctum' => [
        'driver' => 'sanctum',
        'provider' => 'users', // 対象となるユーザー情報
    ],
],

ユーザー情報を取得するデータベース設定

'providers' => [
    'users' => [
        'driver' => 'eloquent',
        'model' => App\Models\User::class,
    ],
],

パスワードリセット

'passwords' => [
    'users' => [
        'provider' => 'users',
        'table' => 'password_resets',
        'expire' => 60,
        'throttle' => 60,
    ],
],

まだまだ勉強中ですので間違っている点や至らぬ点がありましたら教えていただけると助かります。

ご覧いただきありがとうございました。

searchbox

スポンサー

ProFile

ame

趣味:読書,プログラミング学習,サイト制作,ブログ

IT嫌いを克服するためにITパスを取得しようと勉強してからサイト制作が趣味に変わりました笑
今はCMSを使わずこのサイトを完全自作でサイト運営中〜

New Article

index