こんにちは、かつコーチです。
前回までのデータ連携編で、axios・VueUse・VeeValidateを使ったAPI連携とフォーム実装を扱いました。
ここからは認証・セキュリティ編に入ります。
第一弾は、Vueアプリに認証機能を組み込む際の代表的な実装パターンです。
すでにログインフォームやAPI連携の実装経験がある方向けに、選択肢の違いと判断軸を中心に解説します。
Vueの認証実装で押さえるべき全体像
認証状態はどこで管理するか
Vueには、ReactのNextAuthのような「公式の認証ライブラリ」は存在しません。
そのため、認証状態の管理方法は自分で設計する必要があります。
代表的には次の3つの要素を組み合わせて実装します。
- トークンの保存場所:ログイン後に発行されたトークンをどこに置くか
- 状態管理:ログイン中かどうかをアプリ全体でどう共有するか(Piniaが定番)
- ルートガード:未ログインユーザーを保護されたページに入れないための制御
トークンベース認証とセッションベース認証
まず、認証方式には大きく2つの選択肢があります。
| 方式 | 仕組み | 向いているケース |
|---|---|---|
| セッション認証 | サーバー側にセッション情報を保持し、Cookieでセッションを識別する | 自社のバックエンドとフロントを一体で作る場合 |
| トークン認証(JWT等) | ログイン時にトークンを発行し、以降のリクエストにトークンを添えて認証する | SPA+API、モバイルアプリとバックエンドを共有する場合 |
VueでSPAを作る場合は、バックエンドがAPIとして独立していることが多く、JWTなどのトークン認証が採用されるケースが目立ちます。
Piniaでログイン状態を管理する
認証ストアを設計する
以前の「Piniaの実践」で扱った設計方針に沿って、認証専用のストアを1つ用意します。
// stores/auth.ts
import { defineStore } from 'pinia'
import { ref, computed } from 'vue'
import { apiClient } from '@/lib/api'
interface User {
id: number
name: string
email: string
}
export const useAuthStore = defineStore('auth', () => {
const token = ref<string | null>(localStorage.getItem('token'))
const user = ref<User | null>(null)
const isAuthenticated = computed(() => token.value !== null)
const login = async (email: string, password: string) => {
const response = await apiClient.post('/auth/login', { email, password })
token.value = response.data.token
user.value = response.data.user
localStorage.setItem('token', response.data.token)
}
const logout = () => {
token.value = null
user.value = null
localStorage.removeItem('token')
}
return { token, user, isAuthenticated, login, logout }
})
isAuthenticatedというcomputedを1つ用意しておくことで、アプリ内のどこからでも「ログイン中かどうか」を一貫した基準で判定できます。
axiosにトークンを自動付与する
すべてのAPIリクエストに毎回トークンを付けて回るのは非効率なので、axiosの「インターセプター」機能を使ってリクエストごとに自動付与します。
// src/lib/api.ts
import axios from 'axios'
import { useAuthStore } from '@/stores/auth'
export const apiClient = axios.create({
baseURL: 'https://api.example.com',
})
apiClient.interceptors.request.use((config) => {
const authStore = useAuthStore()
if (authStore.token) {
config.headers.Authorization = `Bearer ${authStore.token}`
}
return config
})
これで、apiClientを経由するすべてのリクエストに自動的に認証ヘッダーが付くようになります。
ルートガードで未ログインユーザーを制御する
Vue RouterのbeforeEachを使う
保護したいページには、metaフィールドでフラグを立てておき、Vue RouterのbeforeEachで一括チェックします。
// router/index.ts
import { createRouter, createWebHistory } from 'vue-router'
import { useAuthStore } from '@/stores/auth'
const router = createRouter({
history: createWebHistory(),
routes: [
{ path: '/login', component: () => import('@/pages/Login.vue') },
{
path: '/mypage',
component: () => import('@/pages/MyPage.vue'),
meta: { requiresAuth: true },
},
],
})
router.beforeEach((to, from, next) => {
const authStore = useAuthStore()
if (to.meta.requiresAuth && !authStore.isAuthenticated) {
next('/login')
} else {
next()
}
})
export default router
ページごとに認証チェックのコードを書くのではなく、ルーティング定義のmeta.requiresAuthとルートガードに集約することで、保護対象の追加・削除がしやすくなります。
よくあるつまずきポイント:リロードでログイン状態が消える
私が実際にハマった「ページを更新すると毎回ログアウトになる」問題
認証機能を初めて実装したとき、Piniaストアのuser情報だけを見てisAuthenticatedを判定するようにしていました。
ログイン直後は問題なく動くのですが、ブラウザをリロードするたびにログイン画面に戻されてしまい、しばらく原因が分かりませんでした。
原因は単純で、Piniaのストアはメモリ上の状態なので、リロードのたびに初期化されてしまうという、当たり前だけれど見落としがちな挙動でした。
トークン自体はlocalStorageに保存できていても、user情報(画面表示用のユーザー名など)まではリロード後に復元する処理を書いていなかったのです。
❌ Before:user情報だけを見て判定し、リロードで状態が失われる
// stores/auth.ts(悪い例)
import { defineStore } from 'pinia'
import { ref, computed } from 'vue'
export const useAuthStore = defineStore('auth', () => {
const user = ref<{ id: number; name: string } | null>(null)
// userはメモリ上の状態なので、リロードすると必ずnullに戻ってしまう
const isAuthenticated = computed(() => user.value !== null)
return { user, isAuthenticated }
})
✅ After:トークンをlocalStorageから復元し、起動時にユーザー情報も取り直す
// stores/auth.ts
import { defineStore } from 'pinia'
import { ref, computed } from 'vue'
import { apiClient } from '@/lib/api'
export const useAuthStore = defineStore('auth', () => {
// 初期化時にlocalStorageからトークンを復元する
const token = ref<string | null>(localStorage.getItem('token'))
const user = ref<{ id: number; name: string } | null>(null)
const isAuthenticated = computed(() => token.value !== null)
const restoreSession = async () => {
if (!token.value) return
try {
const response = await apiClient.get('/auth/me')
user.value = response.data
} catch (error) {
// トークンが無効な場合はログアウト状態に戻す
token.value = null
user.value = null
localStorage.removeItem('token')
}
}
return { token, user, isAuthenticated, restoreSession }
})
isAuthenticatedの判定基準を「localStorageから復元できるトークンの有無」にし、アプリ起動時(App.vueのonMountedなど)でrestoreSessionを呼ぶことで、リロード後も認証状態が保たれるようになります。
「ログイン中かどうかの判定基準を、メモリ上の情報だけに頼らない」というのは、認証機能を実装する上でとても重要なポイントです。
まとめ
この記事のポイント
- Vueには公式の認証ライブラリがなく、トークン保存・状態管理・ルートガードを自分で組み合わせて設計する
- SPA+APIの構成では、セッション認証よりトークン認証(JWTなど)が採用されやすい
- Piniaで認証ストアを作り、axiosのインターセプターでトークンを自動付与すると管理がシンプルになる
- Vue Routerの
beforeEachとmeta.requiresAuthで、保護対象ページの認証チェックを一元化できる - 認証状態の判定をメモリ上の情報だけに頼ると、リロードでログイン状態が消える事故につながる
次に読むべき記事
次回は「VueアプリのXSS対策」を解説します。
認証機能と合わせて押さえておきたい、フロントエンド側のセキュリティ対策です。
→ 次の記事:VueアプリのXSS対策