💡
LaravelとVueでログイン認証実装
Laravel + Vue.js で API 認証を実装する方法
はじめに
この記事では、Laravel と Vue.js を使用して API 認証を実装する方法について解説します。Laravel Sanctum を使用することで、安全で使いやすい API 認証システムを構築していきましょう。
目次
- 環境構築
- Laravel Sanctum の設定
- 認証用 API の実装
- Vue.js 側の実装
- 認証状態の管理
- まとめ
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 認証の実装方法について解説しました。主なポイントは以下の通りです:
-
環境構築
- Laravel Sanctum のインストール
- 必要なパッケージのセットアップ
-
認証システムの実装
- トークンベースの認証
- セッション管理
- セキュアな API 通信
-
フロントエンド実装
- Pinia を使用した状態管理
- ルーティング制御
- エラーハンドリング
セキュリティに関する注意点
- 本番環境では適切な CORS 設定が必要です
- トークンの有効期限は適切に設定してください
- セッションストレージの代わりに HttpOnly Cookie の使用を検討してください
次のステップ
- パスワードリセット機能の実装
- メール認証の追加
- 2 要素認証の実装
参考リンク
この記事が皆様の開発のお役に立てれば幸いです。
Discussion