この記事の目的
- 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以降は標準搭載されているみたいです)
- ②ライブラリに必要な
migrations
とconfig
を作成 - ③カーネルにミドルウェアの設定をする(SPAを認証する場合)
- ④設定ファイルの確認
- ⑤ログインユーザーのデータの追加
- ⑥Seederでユーザーデータ作成
- ⑦ルートの設定
- ⑧ログイン/ログアウトのAPI作成
- ⑨動作確認
※本記事ではLaravel Sanctumの使い方をメインとしているのでReact側でAPIを呼ぶ実装についての説明は詳しくは行なっていないです。
①Laravel Sanctumインストール
【公式ドキュメント】Laravel 9.x Laravel Sanctumに沿ってインストールをしていきます
composer require laravel/sanctum
※Laravel8.6以降からLaravel Sanctumが標準でインストールされるので不要です。
②ライブラリに必要なmigrations
とconfig
を作成
php artisan vendor:publish --provider="Laravel\Sanctum\SanctumServiceProvider"
※Laravel8.6以降からLaravel Sanctumが標準でインストールされるので不要です。
APIトークンを使用する場合はphp artisan migrate
を実行しますが、今回はSPAを作成するためこちらは実行しません
③カーネルにミドルウェアの設定をする(SPAを認証する場合)
'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
に変更
'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でユーザーデータ作成
認証用のユーザーデータ作成
認証用に決まったユーザーのデータを作成(自分で決めた認証用データ)
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ファイルで呼び出し
public function run()
{
$this->call(TaskSeeder::class);
$this->call(UserSeeder::class);
}
※TaskSeederについてはTaskも別で設定しているため記載があります。本記事とは関係はないです
seedコマンドでユーザー作成
ユーザーのみの作成で良いのでオプションでUserSeeder
を指定しています
php artisan db:seed --class=UserSeeder
⑦ルートの設定
認証を必要とする機能をRoute::middleware('auth:sanctum')->get('/user', function (Request $request) {認証を必要とする機能});
の中に記載する。
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
を下記のように編集
<?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
を下記のように編集
<?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
を下記のように編集
<?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.',
]);
}
}
ログイン/ログアウト機能のルートを通す
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
上記コマンドでルートを確認しログインログアウトのルートが追加されているかと思います
⑨動作確認
ここまででLaravel Sanctumを使用したSPA認証の設定は完了したので、画面で動作確認を行います。
補足
今回Reactでタスク管理アプリを作成し、そこにLaravel Sanctumを使用したSPA認証を追加しています。
追加したログインAPIをaxiosで飛び出している部分について参考としてReact側の該当部分のみ下記にコードを載せています。
ログイン画面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 クリックして表示
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 クリックして表示
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;
}