【Vue】Vueアプリに認証機能を実装するパターン

JavaScript

こんにちは、かつコーチです。

前回までのデータ連携編で、axios・VueUse・VeeValidateを使ったAPI連携とフォーム実装を扱いました。

ここからは認証・セキュリティ編に入ります。

第一弾は、Vueアプリに認証機能を組み込む際の代表的な実装パターンです。

すでにログインフォームやAPI連携の実装経験がある方向けに、選択肢の違いと判断軸を中心に解説します。

Vueの認証実装で押さえるべき全体像

認証状態はどこで管理するか

Vueには、ReactのNextAuthのような「公式の認証ライブラリ」は存在しません。

そのため、認証状態の管理方法は自分で設計する必要があります。

代表的には次の3つの要素を組み合わせて実装します。

  1. トークンの保存場所:ログイン後に発行されたトークンをどこに置くか
  2. 状態管理:ログイン中かどうかをアプリ全体でどう共有するか(Piniaが定番)
  3. ルートガード:未ログインユーザーを保護されたページに入れないための制御

トークンベース認証とセッションベース認証

まず、認証方式には大きく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.vueonMountedなど)でrestoreSessionを呼ぶことで、リロード後も認証状態が保たれるようになります。

「ログイン中かどうかの判定基準を、メモリ上の情報だけに頼らない」というのは、認証機能を実装する上でとても重要なポイントです。

まとめ

この記事のポイント

  • Vueには公式の認証ライブラリがなく、トークン保存・状態管理・ルートガードを自分で組み合わせて設計する
  • SPA+APIの構成では、セッション認証よりトークン認証(JWTなど)が採用されやすい
  • Piniaで認証ストアを作り、axiosのインターセプターでトークンを自動付与すると管理がシンプルになる
  • Vue RouterのbeforeEachmeta.requiresAuthで、保護対象ページの認証チェックを一元化できる
  • 認証状態の判定をメモリ上の情報だけに頼ると、リロードでログイン状態が消える事故につながる

次に読むべき記事

次回は「VueアプリのXSS対策」を解説します。

認証機能と合わせて押さえておきたい、フロントエンド側のセキュリティ対策です。

→ 次の記事:VueアプリのXSS対策

タイトルとURLをコピーしました