LaravelでSPA認証を実装する

LaravelでSPA認証を実装する

概要

LaravelでのSPA認証の実装方法の覚書。 使用技術は以下の通り。

  • 手動による認証+SanctumによるSPA認証
  • セッション+クッキー認証

環境

  • PHPのバージョン
    • 8.1.27
  • Laravelのバージョン
    • v9.52.16
  • Sanctumのバージョン
    • v3.3.3

参照ドキュメント

構成

今回はログイン画面はSSRで実装しログイン後はSPAとなる構成としている。

実装手順

  1. ログインの実装
    1. ログイン画面はSSRで実装する
  2. ログアウトの実装
  3. ルートを認証で保護する
  4. フロントの実装

ログインの実装

ログインAPIの実装

ログイン用のコントローラを実装する。実装コードはほぼ以下URLの「ユーザーを手作業で認証する」の通り実装。コード中の詳細についてもリンク先を参照。

https://readouble.com/laravel/9.x/ja/authentication.html

<?php

namespace App\Http\Controllers;

use Illuminate\Contracts\View\View;
use Illuminate\Http\Request;
use Illuminate\Http\RedirectResponse;
use Illuminate\Support\Facades\Auth;

class LoginController extends Controller
{
    /**
     * 認証の試行を処理
     */
    public function authenticate(Request $request): RedirectResponse
    {
        $credentials = $request->validate([
            'email' => ['required', 'email'],
            'password' => ['required'],
        ]);

        if (Auth::attempt($credentials)) {
            $request->session()->regenerate();

            return redirect()->intended('/');
        }

        return back()->withErrors([
            'email' => 'The provided credentials do not match our records.',
        ])->onlyInput('email');
    }
}

web.phpにルーティングを追加する。

Route::post('/login', [LoginController::class, 'authenticate']);

ログイン画面の実装

ログイン画面はSSRで実現するためLaravelのBladeを使用する。

resources/views/login.blade.phpを作成する

<!DOCTYPE html>
<html lang="{{ str_replace('_', '-', app()->getLocale()) }}">

<head>
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1" />

    <title>Login</title>
</head>

<body>
    <section>
        <h1>Login</h1>

        <div>
            <form action="{{ url('/login') }}" method="POST">
                {{ csrf_field() }}
                <div>
                    <p>email</p>
                    <input type="email" name="email" value="{{ old('email') }}" required autofocus>

                    @error('email')
                        <div>
                            {{ $message }}
                        </div>
                    @enderror

                </div>
                <div>
                    <p>password</p>
                    <input type="text" name="password">
                </div>
                <div>
                    <!-- 送信ボタン -->
                    <input type="submit" value="送信">
                </div>
            </form>
        </div>

    </section>

</body>

</html>
  • formタグで先ほど作成した認証ルートにCSRFトークン付きでPOST
  • 認証がエラーした場合はフラッシュメッセージとして@error('email')部分を表示

ログイン画面のコントローラを作る。LoginController.phpに以下のメソッドを追加する。

    public function index(): View
    {
        return view('login');
    }

web.phpにルーティングを追加する。

Route::get('/login', [LoginController::class, 'index'])->name('login');

ログイン先の画面を作る

ログイン成功時の遷移先の画面を作る。resources/views/index.blade.phpを作る。後ほどSPAログインに変更する際にまた書きかえるので一旦は遷移したことがわかる程度の内容にする。

<! DOCTYPE html>
<html>
<head></head>
<body>
    <div>ログインしました</div>
</body>
</html>

web.phpにルーティングを追加する。

Route::get('/', function () {
    return view('index');
});

ログアウトの実装

ログアウトの処理を作成する

コントローラの作成。 これも基本的には以下の参照先に記載の「ログアウト」の項目の通り実装する。

https://readouble.com/laravel/9.x/ja/authentication.html

<?php

namespace App\Http\Controllers;

use Illuminate\Http\Request;
use Illuminate\Http\RedirectResponse;
use Illuminate\Support\Facades\Auth;

class LogoutController extends Controller
{
    /**
     * ユーザーをアプリケーションからログアウトさせる
     */
    public function logout(Request $request): RedirectResponse
    {
        Auth::logout();

        $request->session()->invalidate();

        $request->session()->regenerateToken();

        return redirect('/login');
    }
}
  • セッションを無効にする
  • CSRFトークンを初期化する
  • ログアウト後の画面にリダイレクトする

コントローラが完成したらweb.phpにルーティングを追加する。

Route::post('/logout', [LogoutController::class, 'logout']);

ログアウトボタンを画面に追加する

ログアウトするためのボタンをindex.blade.phpに追加する。

<! DOCTYPE html>
<html>
<head></head>
<body>
    <div>ログインしました</div>
    <div>
        <form action="{{ url('/logout') }}" method="POST">
            {{ csrf_field() }}
            <div>
                <input type="submit" value="ログアウト">
            </div>
        </form>
    </div>
</body>
</html>

ログイン後、ログアウトボタンを押すとログイン画面にリダイレクトされればOK。

ルートを認証で保護する

ログイン状態でないと/にはアクセスできないようにweb.php実装する。参照先の「ルートの保護」の通り実装する。

https://readouble.com/laravel/9.x/ja/authentication.html

Route::get('/', function () {
    // 認証済みユーザーのみがこのルートにアクセス可能
    return view('index');
})->middleware('auth');

->middleware()の追加でこのルートには認証済みのユーザしかアクセスできないようになる。

フロントの実装

ここまででSSRによる認証の機構は完成したので、最終のゴール地点である「ログインまでSSRで行いログイン後はSPAとして動かす」 ところまでを実装する。なお、フロントのフレームワークとしてはVue.jsを使用するが、こちらの細かい実装内容については割愛する。

index.blade.phpにVueを埋め込めるよう変更した上、resources/js/components/App.vueを以下の通り実装する。

<template>
    <div>
        <p>Vueのページ</p>
    </div>
    <div>
        <button type="button" v-on:click="logout"> ログアウト </button>
    </div>
</template>

<script lang="ts" setup>
import axios from "axios";

const logout = (): void => {
    axios.post("logout");
    window.location.href = "/login";
};
</script>

ログアウトをリクエスト後、window.location.hrefを更新してフロント側で自前でロケーションを切り替える必要がある。

Sanctumについて

フロント側で認証する場合、Sanctumによる以下の手順が必要だが。

  1. Sanctumのミドルウェアapp/Http/Kernel.phpに追加
  2. ログイン前にsanctum/csrf-cookieにアクセスしCSRFトークンを初期化
    • XSRF-TOKENを取得する
  3. リクエスト時にXSRF-TOKENをヘッダに付与する
  4. ログイン後にアクセスするルーティングをSanctumミドルウェアで保護する

1に関しては自分の環境ではLaravelにデフォルトで設定されていた。 throttle:apiが該当。

app/Http/Kernel.php

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

2のsanctum/csrf-cookieのルーティングもデフォルトで設定されている。 また、初めのSSRの認証時にCSRFトークンを取得するため、特に以下へのアクセスは本手順では不要。

php artisan route:list

  GET|HEAD   sanctum/csrf-cookie ............................................... sanctum.csrf-cookie › Laravel\Sanctum › CsrfCookieController@show

3のトークンの付与はaxiosなどのライブラリを使用した場合自動で付与してくれる。

4のログイン後にアクセスするルーティングの保護はroutes/api.phpにデフォルトで参考となるソースがあるためこちらを元に実装する。

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

以上でLaravelによるSPA認証の実装が完了。