💡

LaravelとVueでログイン認証実装

に公開

Laravel + Vue.js で API 認証を実装する方法

はじめに

この記事では、Laravel と Vue.js を使用して API 認証を実装する方法について解説します。Laravel Sanctum を使用することで、安全で使いやすい API 認証システムを構築していきましょう。

目次

  1. 環境構築
  2. Laravel Sanctum の設定
  3. 認証用 API の実装
  4. Vue.js 側の実装
  5. 認証状態の管理
  6. まとめ

1. 環境構築

必要なパッケージのインストール

# Laravel側
composer require laravel/sanctum
php artisan vendor:publish --provider="Laravel\Sanctum\SanctumServiceProvider"
php artisan migrate

# Vue.js側
npm install axios pinia vue-router

2. Laravel Sanctum の設定

User モデルの設定

<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Foundation\Auth\User as Authenticatable;
use Illuminate\Notifications\Notifiable;
use Laravel\Sanctum\HasApiTokens;

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

    // 既存のコード...
}

環境変数の設定

.envファイルに以下の設定を追加します:

SANCTUM_STATEFUL_DOMAINS=localhost:8000,localhost:8081
SESSION_DOMAIN=localhost

3. 認証用 API の実装

認証コントローラーの作成

<?php

namespace App\Http\Controllers;

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' => ['メールアドレスまたはパスワードが正しくありません。'],
            ]);
        }

        // 既存のトークンを削除し、新しいトークンを発行
        $user->tokens()->delete();
        $token = $user->createToken('auth_token', ['*'], now()->addHours(24));

        return response()->json([
            'token' => $token->plainTextToken,
            'user' => $user,
            'expires_at' => now()->addHours(24)->toDateTimeString(),
        ]);
    }

    /**
     * ログアウト処理
     */
    public function logout(Request $request)
    {
        $request->user()->currentAccessToken()->delete();
        return response()->json(['message' => 'ログアウトしました。']);
    }

    /**
     * ユーザー情報取得
     */
    public function me(Request $request)
    {
        return response()->json($request->user());
    }
}

ルートの設定

<?php

use App\Http\Controllers\AuthController;
use Illuminate\Support\Facades\Route;

// 認証なしでアクセス可能
Route::post('/login', [AuthController::class, 'login']);

// 認証が必要なルート
Route::middleware('auth:sanctum')->group(function () {
    Route::post('/logout', [AuthController::class, 'logout']);
    Route::get('/me', [AuthController::class, 'me']);
});

4. Vue.js 側の実装

認証用ストアの作成

import { defineStore } from "pinia";
import axios from "axios";

const API_URL = "http://localhost:8000/api";

const axiosInstance = axios.create({
  baseURL: API_URL,
  headers: {
    "Content-Type": "application/json",
    Accept: "application/json",
  },
});

export const useAuthStore = defineStore("auth", {
  state: () => ({
    token: sessionStorage.getItem("token") || null,
    user: JSON.parse(sessionStorage.getItem("user") || "null"),
    expiresAt: sessionStorage.getItem("expiresAt") || null,
  }),

  getters: {
    isAuthenticated: (state) => !!state.token && !!state.user,
    isTokenExpired: (state) => {
      if (!state.expiresAt) return true;
      return new Date(state.expiresAt) < new Date();
    },
  },

  actions: {
    async login(credentials) {
      try {
        const response = await axiosInstance.post("/login", credentials);

        this.token = response.data.token;
        this.user = response.data.user;
        this.expiresAt = response.data.expires_at;

        // セッションストレージに保存
        sessionStorage.setItem("token", this.token);
        sessionStorage.setItem("user", JSON.stringify(this.user));
        sessionStorage.setItem("expiresAt", this.expiresAt);

        this.setAxiosAuthHeader();

        return response;
      } catch (error) {
        return Promise.reject(error);
      }
    },

    async logout() {
      try {
        if (this.token) {
          await axiosInstance.post("/logout");
        }
      } catch (error) {
        console.error("ログアウト中にエラーが発生しました:", error);
      } finally {
        this.clearAuth();
      }
    },

    setAxiosAuthHeader() {
      if (this.token) {
        axiosInstance.defaults.headers.common[
          "Authorization"
        ] = `Bearer ${this.token}`;
      } else {
        delete axiosInstance.defaults.headers.common["Authorization"];
      }
    },

    clearAuth() {
      this.token = null;
      this.user = null;
      this.expiresAt = null;

      sessionStorage.removeItem("token");
      sessionStorage.removeItem("user");
      sessionStorage.removeItem("expiresAt");

      delete axiosInstance.defaults.headers.common["Authorization"];
    },
  },
});

ログインコンポーネントの実装

<template>
  <div class="login-container">
    <div class="login-form">
      <h1>ログイン</h1>

      <div v-if="error" class="error-message">
        {{ error }}
      </div>

      <form @submit.prevent="handleLogin">
        <div class="form-group">
          <label for="email">メールアドレス</label>
          <input
            type="email"
            id="email"
            v-model="form.email"
            required
            autocomplete="username"
          />
        </div>

        <div class="form-group">
          <label for="password">パスワード</label>
          <input
            type="password"
            id="password"
            v-model="form.password"
            required
            autocomplete="current-password"
          />
        </div>

        <button type="submit" :disabled="loading">
          {{ loading ? "ログイン中..." : "ログイン" }}
        </button>
      </form>
    </div>
  </div>
</template>

<script setup>
import { ref } from "vue";
import { useRouter, useRoute } from "vue-router";
import { useAuthStore } from "@/stores/auth";

const router = useRouter();
const route = useRoute();
const authStore = useAuthStore();

const form = ref({
  email: "",
  password: "",
});

const loading = ref(false);
const error = ref("");

const handleLogin = async () => {
  loading.value = true;
  error.value = "";

  try {
    await authStore.login(form.value);
    const redirectPath = route.query.redirect || "/dashboard";
    router.push(redirectPath);
  } catch (err) {
    if (err.response?.data?.message) {
      error.value = err.response.data.message;
    } else if (err.response?.data?.errors) {
      const errorMessages = Object.values(err.response.data.errors);
      error.value = errorMessages.flat().join("\n");
    } else {
      error.value = "ログインに失敗しました。再度お試しください。";
    }
  } finally {
    loading.value = false;
  }
};
</script>


5. 認証状態の管理

認証用ミドルウェア

import { useAuthStore } from "@/stores/auth";

export function authGuard(to, from, next) {
  const authStore = useAuthStore();
  const isAuthenticated = authStore.checkTokenExpiration();

  if (to.meta.requiresAuth && !isAuthenticated) {
    return next({ name: "login", query: { redirect: to.fullPath } });
  } else if (to.meta.requiresGuest && isAuthenticated) {
    return next({ name: "dashboard" });
  }

  return next();
}

Axios インターセプターの設定

import axios from "axios";
import { useAuthStore } from "@/stores/auth";

const axiosInstance = axios.create({
  baseURL: "http://localhost:8000/api",
  headers: {
    "Content-Type": "application/json",
    Accept: "application/json",
  },
});

// レスポンスインターセプター
axiosInstance.interceptors.response.use(
  (response) => response,
  async (error) => {
    if (error.response?.status === 401) {
      const authStore = useAuthStore();
      authStore.clearAuth();
      window.location.href = "/login";
    }
    return Promise.reject(error);
  }
);

// リクエストインターセプター
axiosInstance.interceptors.request.use(
  (config) => {
    const authStore = useAuthStore();
    const token = authStore.token;

    if (token) {
      config.headers["Authorization"] = `Bearer ${token}`;
    }

    return config;
  },
  (error) => Promise.reject(error)
);

export default axiosInstance;

6. まとめ

この記事では、Laravel Sanctum を使用した API 認証の実装方法について解説しました。主なポイントは以下の通りです:

  1. 環境構築

    • Laravel Sanctum のインストール
    • 必要なパッケージのセットアップ
  2. 認証システムの実装

    • トークンベースの認証
    • セッション管理
    • セキュアな API 通信
  3. フロントエンド実装

    • Pinia を使用した状態管理
    • ルーティング制御
    • エラーハンドリング

セキュリティに関する注意点

  • 本番環境では適切な CORS 設定が必要です
  • トークンの有効期限は適切に設定してください
  • セッションストレージの代わりに HttpOnly Cookie の使用を検討してください

次のステップ

  1. パスワードリセット機能の実装
  2. メール認証の追加
  3. 2 要素認証の実装

参考リンク


この記事が皆様の開発のお役に立てれば幸いです。

Discussion