LoginSignup
6
4

More than 1 year has passed since last update.

【Laravel Sanctum】SPA認証

Last updated at Posted at 2023-03-09

この記事の目的

  • Laravel9とReact18,axiosを使用したSPAアプリを作成したときの備忘録
  • SPA用の認証機能実装時について
  • Laravel Sanctumについて

環境

以下のバージョンで行なっています

  • PHP: 8.2.3
  • Laravel: 9.52.4
  • Laravel Sanctum: 3.2.1
  • React: 18.2.0
  • axios: 1.3.4

概要

記事の内容は大きく以下の形で記載しています

  • laravelの認証機能
  • Laravel Sanctumとは
  • Laravel Sanctum使い方
  • まとめ

laravelの認証機能

laravelの認証機能には下記のようにいくつか種類があるみたいです。

laravelの認証機能
Laravel UI
Laravel Socialite
Laravel Passport
Laravel Sanctum
Laravel Breeze
Laravel Fortify
Laravel Jetstream

詳しい説明については参考にしたサイトを下記に載せているのでそちらを参考に
Laravelの認証系パッケージを整理する
こちらの記事によるとSPA作成する際は「Laravel Sanctum」を使用すると良いみたいなのでこちらを実装していきます

Laravel Sanctumとは

Laravel Sanctum(サンクタム、聖所)は、SPA(シングルページアプリケーション)、モバイルアプリケーション、およびシンプルなトークンベースのAPIへ、軽い認証システムを提供します。Sanctumを使用すればアプリケーションの各ユーザーは、自分のアカウントに対して複数のAPIトークンを生成できます。これらのトークンには、そのトークンが実行できるアクションを指定するアビリティ/スコープが付与されることもあります。

公式では上記のように書かれています。
ちょっとよくわからないので崩していうと

モバイルアプリや SPA 作成時に使用されるライブラリ。APIトークン認証(主にモバイルアプリ用)、クッキー認証(主にSPA用)ができる

ちなみにLaravel SanctumはLaravel7から追加された機能みたいです。Laravel7ではLaravel Airlockという名前だったみたいです。

認証の種類は2つ

  • APIトークン
  • クッキーベースのセッション認証

こちらのどちらかを使用する感じだと思います。

Laravel Sanctum使い方

  • ①Laravel Sanctumインストール(※Laravel8.6以降は標準搭載されているみたいです)
  • ②ライブラリに必要なmigrationsconfigを作成
  • ③カーネルにミドルウェアの設定をする(SPAを認証する場合)
  • ④設定ファイルの確認
  • ⑤ログインユーザーのデータの追加
  • ⑥Seederでユーザーデータ作成
  • ⑦ルートの設定
  • ⑧ログイン/ログアウトのAPI作成
  • ⑨動作確認
    ※本記事ではLaravel Sanctumの使い方をメインとしているのでReact側でAPIを呼ぶ実装についての説明は詳しくは行なっていないです。

①Laravel Sanctumインストール

【公式ドキュメント】Laravel 9.x Laravel Sanctumに沿ってインストールをしていきます

composer require laravel/sanctum

※Laravel8.6以降からLaravel Sanctumが標準でインストールされるので不要です。

②ライブラリに必要なmigrationsconfigを作成

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

※Laravel8.6以降からLaravel Sanctumが標準でインストールされるので不要です。
APIトークンを使用する場合はphp artisan migrateを実行しますが、今回はSPAを作成するためこちらは実行しません

③カーネルにミドルウェアの設定をする(SPAを認証する場合)

app/Http/Kernel.php
'api' => [
    \Laravel\Sanctum\Http\Middleware\EnsureFrontendRequestsAreStateful::class, // 追記した部分
    'throttle:api',
    \Illuminate\Routing\Middleware\SubstituteBindings::class,
],

※laravel9では\Laravel\Sanctum\Http\Middleware\EnsureFrontendRequestsAreStateful::class,がコメントアウトだったのでコメントアウトを外すだけで問題なかった。
記載がなければ【公式ドキュメント】Laravel 9.x Laravel Sanctumからコピーして貼り付ける。

④設定ファイルの確認

SPAがリクエストを行うドメインを設定を行う
localhost:3000の部分をlocalhost:8000に変更

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

⑤ログインユーザーのデータの追加

database/migrations/2019_12_14_000001_create_personal_access_tokens_table.phpを削除
(APIトークン認証を使用する際に必要になり今回は不必要なので削除する)

seederファイルを作成

php artisan make:seeder UserSeeder

⑥Seederでユーザーデータ作成

認証用のユーザーデータ作成

認証用に決まったユーザーのデータを作成(自分で決めた認証用データ)

database/seeders/UserSeeder.php
public function run()
{
    \DB::table('users')->insert([
        [
            'name'              => 'admin',
            'email'             => 'admin@example.com',
            'email_verified_at' => now(),
            'password'          => \Hash::make('password1234'),
            'created_at'        => now(),
            'updated_at'        => now(),
        ],
        [
            'name'              => 'pikimaru',
            'email'             => 'pikimaru@example.com',
            'email_verified_at' => now(),
            'password'          => \Hash::make('password1234'),
            'created_at'        => now(),
            'updated_at'        => now(),
        ],
    ]);
}

※usersテーブルの構成は異なる場合がありますので、ご自身の作成したusersテーブルに合わせて作成してください。

seedersファイルで呼び出し

database/seeders/DatabaseSeeder.php
public function run()
{
    $this->call(TaskSeeder::class);
    $this->call(UserSeeder::class);
}

※TaskSeederについてはTaskも別で設定しているため記載があります。本記事とは関係はないです

seedコマンドでユーザー作成

ユーザーのみの作成で良いのでオプションでUserSeederを指定しています

php artisan db:seed --class=UserSeeder

ユーザーが登録されているかの確認
スクリーンショット 2023-03-05 22.44.35.png

⑦ルートの設定

認証を必要とする機能をRoute::middleware('auth:sanctum')->get('/user', function (Request $request) {認証を必要とする機能});の中に記載する。

routes/api.php
Route::middleware('auth:sanctum')->group(function () {
    //認証を必要とする機能を記載
    Route::apiResource('tasks', 'App\Http\Controllers\TaskController');
    Route::patch('tasks/update-done/{task}', 'App\Http\Controllers\TaskController@updateDone');
    Route::get('user', function (Request $request) {
        return $request->user();
    });
});

⑧ログイン/ログアウトのAPI作成

下記のコマンドでコントローラー作成

$ php artisan make:request Auth/LoginRequest
$ php artisan make:controller -i Auth/LoginController
$ php artisan make:controller -i Auth/LogoutController

リクエストでバリデーション設定

作成したapp/Http/Requests/Auth/LoginRequest.phpを下記のように編集

app/Http/Requests/Auth/LoginRequest.php
<?php declare(strict_types=1);

namespace App\Http\Requests\Auth;

use Illuminate\Foundation\Http\FormRequest;

final class LoginRequest extends FormRequest
{
    /**
     * Determine if the user is authorized to make this request.
     *
     * @return bool
     */
    public function authorize(): bool
    {
        return true;
    }

    /**
     * Get the validation rules that apply to the request.
     *
     * @return array
     */
    public function rules(): array
    {
        return [
            'email' => ['required', 'email'],
            'password' => ['required', 'min:6'],
        ];
    }
}

ログイン機能実装

作成したapp/Http/Controllers/Auth/LoginController.php を下記のように編集

app/Http/Controllers/Auth/LoginController.php
<?php
declare(strict_types=1);

namespace App\Http\Controllers\Auth;

use App\Http\Controllers\Controller;
use App\Http\Requests\Auth\LoginRequest;
use Illuminate\Auth\AuthenticationException;
use Illuminate\Auth\AuthManager;
use Illuminate\Http\JsonResponse;

final class LoginController extends Controller
{
    /**
     * @param AuthManager $auth
     */
    public function __construct(
        private readonly AuthManager $auth,
    ) {
    }

    /**
     * @param LoginRequest $request
     * @return JsonResponse
     * @throws AuthenticationException
     */
    public function __invoke(LoginRequest $request): JsonResponse
    {
        // リクエストからemailとpasswordの値を取得
        $credentials = $request->only(['email', 'password']);

        // 認証開始
        if ($this->auth->guard()->attempt($credentials)) {
            // セッションIDを再生成
            $request->session()->regenerate();

            // レスポンスを返す
            return new JsonResponse([
                'message' => 'Authenticated.',
            ]);
        }

        // 認証エラーが発生した場合に例外を投げる
        throw new AuthenticationException();
    }
}

ログアウト機能実装

作成したapp/Http/Controllers/Auth/LogoutController.phpを下記のように編集

app/Http/Controllers/Auth/LogoutController.php
<?php
declare(strict_types=1);

namespace App\Http\Controllers\Auth;

use App\Http\Controllers\Controller;
use Illuminate\Auth\AuthManager;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;

final class LogoutController extends Controller
{
    /**
     * @param AuthManager $auth
     */
    public function __construct(
        private readonly AuthManager $auth,
    ) {
    }

    /**
     * @param Request $request
     * @return JsonResponse
     */
    public function __invoke(Request $request): JsonResponse
    {
        // ユーザーが認証されていない場合
        if ($this->auth->guard()->guest()) {
            return new JsonResponse([
                'message' => 'Already Unauthenticated.',
            ]);
        }

        // ログアウトする
        $this->auth->guard()->logout();
        // セッションを無効にする
        $request->session()->invalidate();
        // CSRFトークンを再生成する
        $request->session()->regenerateToken();

        return new JsonResponse([
            'message' => 'Unauthenticated.',
        ]);
    }
}

ログイン/ログアウト機能のルートを通す

routes/api.php
use App\Http\Controllers\Auth\LoginController; //追記部分
use App\Http\Controllers\Auth\LogoutController; //追記部分
Route::post('/login', LoginController::class)->name('login'); //追記部分
Route::post('/logout', LogoutController::class)->name('logout'); //追記部分

Route::middleware('auth:sanctum')->group(function () {
    //認証を必要とする機能
    Route::apiResource('tasks', 'App\Http\Controllers\TaskController');
    Route::patch('tasks/update-done/{task}', 'App\Http\Controllers\TaskController@updateDone');
    Route::get('user', function (Request $request) {
        return $request->user();
    });
});

ルート確認

php artisan route:list

上記コマンドでルートを確認しログインログアウトのルートが追加されているかと思います
スクリーンショット 2023-03-09 23.52.48.png

⑨動作確認

ここまででLaravel Sanctumを使用したSPA認証の設定は完了したので、画面で動作確認を行います。
動作確認.gif

補足

今回Reactでタスク管理アプリを作成し、そこにLaravel Sanctumを使用したSPA認証を追加しています。
追加したログインAPIをaxiosで飛び出している部分について参考としてReact側の該当部分のみ下記にコードを載せています。

ログイン画面resources/ts/pages/login/index.tsx クリックして表示
resources/ts/pages/login/index.tsx
import { useState } from 'react';
import { useLogin } from '../../queries/AuthQuery';

export const LoginPage = () => {
    const login = useLogin();
    const [email, setEmail] = useState('admin@example.com');
    const [password, setPassword] = useState('password1234');

    const handleLogin = (event: React.FormEvent<HTMLFormElement>) => {
        event.preventDefault();
        console.log(email);
        console.log(password);
        login.mutate({ email, password });
    }

    return (
        <>
            <div className="login-page">
                <div className="login-panel">
                    <form onSubmit={handleLogin}>
                        <div className="input-group">
                            <label>メールアドレス</label>
                            <input
                                type="email"
                                className="input" 
                                value={email}
                                onChange={e => setEmail(e.target.value)}
                            />
                        </div>
                        <div className="input-group">
                            <label>パスワード</label>
                            <input
                                type="password"
                                className="input"
                                value={password}
                                onChange={e => setPassword(e.target.value)}
                            />
                        </div>
                        <button type="submit" className="btn">ログイン</button>
                    </form>
                </div>
                <div className="links"><a href="#">ヘルプ</a></div>
            </div>
        </>
    );
}
ログインAPI呼び出し成功/失敗時の処理記載部分resources/ts/queries/AuthQuery.tsx クリックして表示
resources/ts/queries/AuthQuery.tsx
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import * as api from '../api/AuthAPI';
import { toast } from 'react-toastify';
import { AxiosError } from 'axios';

export const useUser = () => {
    return useQuery(['users'], api.getUsers);
};

const useMutationWithToast = (
    mutationFn: (data: any) => Promise<any>,
    successMessage: string,
    errorMessage: string
) => {
    const queryClient = useQueryClient();
    
    return useMutation(mutationFn, {
        onSuccess: () => {
            queryClient.invalidateQueries(['Users']);
            toast.success(successMessage);
        },
        onError: (error: AxiosError) => {
            const { data } = error.response!;
            if (data.errors) {
                Object.values(data.errors).forEach((messages: string[]) => {
                messages.forEach((message: string) => {
                    toast.error(message);
                });
            });
            } else {
                toast.error(errorMessage);
            }
        },
    });
};

export const useLogin = () => {
    return useMutationWithToast(
        api.login,
        'ログインに成功しました',
        'ログインに失敗しました'
    );
};

export const useLogout = () => {
    return useMutationWithToast(
        api.logout,
        'ログアウトに成功しました',
        'ログアウトに失敗しました'
    );
};

axiosを使用してログインログアウトAPI呼び出し部分resources/ts/api/AuthAPI.tsx クリックして表示
resources/ts/api/AuthAPI.tsx
import axios from 'axios';
import { User } from '../types/User';

// getUser 関数の宣言、非同期関数として定義される
export const getUser = async () =>{
    // api/userからUser型のレスポンスを受け取る
    const { data } = await axios.get<User>('api/user');
    // 取得したデータを返す
    return data;
}

// login 関数の宣言、非同期関数として定義される。引数には email と password の文字列型を指定する
export const login = async ({ email, password }: { email: string, password: string }) =>{
    try {
        // /api/loginにemailとpasswordのデータを送信しUser型のレスポンスを受け取る
        const { data } = await axios.post<User>(
            `/api/login`,{ email, password }
        );
        return data;
    } catch (error) {
        // エラーログを出力し、エラーを投げる
        console.error(error);
        throw error;
    }
}

// logout 関数の宣言、非同期関数として定義される
export const logout = async () =>{
    // /api/logoutにリクエストを送信しUser型のレスポンスを受け取る
    const { data } = await axios.post<User>(`/api/logout`);
    return data;
}

参考サイト

6
4
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
6
4