BLOG

ブログ

【Nuxt3】権限ごとのページアクセスの制御について(ログイン機能編)

こちらは、Mavs Advent Calendar2023の11日目の記事です!🧦

ハコザキです。
WEBシステム開発において、ログインが必須な管理画面を実装する際、
管理者しかアクセスさせたくない!など
複数の権限でのページアクセスの制御を実装する機会があると思います。

今回はNuxt3で各機能を活用し、実装してみましたので
ぜひ参考にしていただければと思います..!
記事が長くなってしまったので
前編として簡単なログイン機能の実装(本記事)、
後編にアクセス制御のミドルウェアの実装などを行います。

はじめに

今回はこちらの記事を一部参考にし、Nuxt3の書き方で実装してみました。
非常にわかりやすかったのでぜひ一読していただきたいです..!
Nuxt.jsで権限管理

今回のログイン認証&権限によるページ制御の実装デモ

権限がないページへアクセスできなくなる
権限がある場合ページにアクセスできる

実装準備

まずはNuxt3の環境構築を行います。
今回はPiniaを使用するので合わせて導入します。
Nodeはv20を使用しています。

# 以下実行し、pnpmを選択
npx nuxi@latest init nuxt3-control-page-access

cd nuxt3-control-page-access

# Pinia関連ライブラリのインストール
pnpm i pinia @pinia/nuxt

nuxt.config.tsを以下のように記述します。

// https://nuxt.com/docs/api/configuration/nuxt-config
export default defineNuxtConfig({
  devtools: { enabled: true },

  modules: ['@pinia/nuxt'],
})

pnpm run dev で以下のように表示されていれば準備okです!

pagesフォルダ内のvueファイルをブラウザ上に表示させるため、
~/app.vue , ~/layouts/default.vue に以下のように記述します。

<template>
  <NuxtLayout>
    <NuxtPage />
  </NuxtLayout>
</template>
<template>
  <div>
    <slot />
  </div>
</template>

これで実装準備は完了です!

実装の大まかな流れ

今回は大きく分けて3つのステップにわけて解説します。

  1. 簡易ログイン機能の実装
  2. ページごとにアクセス可能な権限の設定
  3. 認証用・権限制御用のミドルウェアの作成

本記事では、1のみを実装し、後編の記事で2, 3を実装します。

簡易ログイン機能の実装

今回は以下をログイン/ログアウトの要件とします。
あくまでも権限によるページのアクセス制御がお伝えしたいことなので、
簡易的なログインにとどめております..!

  • ログインボタンをクリックすると、Token(ランダム文字列)をCookieに保存する
  • ログアウトボタンをクリックするとCookieを削除する
  • CookieにTokenがあればログイン中とする

また、ログインユーザー情報はPiniaで保持し、
権限は 管理者一般ユーザー の2つを定義します。

以下の流れで実装を進めたいと思います。

  1. 静的ページの実装
  2. ログインユーザーの権限種類の定義
  3. ログイン/ログアウト機能の実装
  4. ログインの保持(リロード時など)

1. 静的ページの実装

まずはログインページ(/login)の
~/pages/login.vueを作成し以下のように記述します。

<script setup lang="ts">
const handleLoginClick = () => {
  navigateTo('/')
}
</script>

<template>
  <h1>ログイン</h1>
  <button type="button" @click="handleLoginClick()">ログイン</button>
</template>

続いて、ログイン後に遷移させる用のページ(/
~/pages/index.vueを作成し以下のように記述します。

<script setup lang="ts">
const isLogin = ref(true)

const handleLogoutClick = () => {
  navigateTo('/login')
}
</script>

<template>
  <h1>管理画面トップ</h1>

  <div v-if="isLogin">
    <p>ようこそ、ログインユーザーさん</p>
    <button type="button" @click="handleLogoutClick">ログアウト</button>
  </div>
</template>

/login にアクセスすると、タイトルとボタンだけのシンプルなページが表示されます。
ボタンをクリックすると、navigateToが実行され / に遷移されます。

ログインページ

/ はログイン後の管理画面トップページです。
isLoginでログイン済みかどうかを判定し、ログイン中であればユーザー名とログアウトボタンを表示させています。
(現在は決め打ちでログイン中にしています。)

ログアウトボタンをクリックすると、ログインページ(/login)へ遷移されます。

2ページがこのように動作していればokです。

2. ユーザー権限の定義

今回は管理者・一般ユーザーの2つの権限があるものとします。
~/utils/constants.ts を作成し、以下のように記述します。

export const ROLES = {
  ADMIN: {
    ID: 1,
    SLUG: 'admin',
    LABEL: '管理者',
  },
  USER: {
    ID: 2,
    SLUG: 'user',
    LABEL: 'ユーザー',
  },
}

このように定数化しておくことで、ユーザー権限のIDやlabelの修正が必要な際、
修正箇所が一箇所で済むようになります。

3. ログイン/ログアウト機能の実装

実装概要でも紹介しましたが、今回は以下の仕組みでログイン・ログアウト機能を再現します。

  • ログインボタンをクリックすると、Token(ランダム文字列)をCookieに保存する
  • ログアウトボタンをクリックするとCookieを削除する
  • CookieにTokenがあればログイン中とする

まずは、以下の2つのストアを定義します。

ログインユーザー情報をPiniaで保持(userストア)
~/store/user.tsを作成し、以下のように記述します。

import { ROLES } from '~/utils/constants'

interface State {
  roleId: number | null
  userName: string
}

/**
 * ユーザー情報
 */
export const useUserStore = defineStore('user', {
  state: (): State => {
    return {
      roleId: null,
      userName: '',
    }
  },
  actions: {
    /**
     * ユーザー情報をクリア
     */
    clearUser() {
      this.$reset()
    },
    /**
     * ユーザー情報をセット
     */
    setUser() {
      const isAdmin = false

      if (isAdmin) {
        this.roleId = ROLES.ADMIN.ID
        this.userName = 'てすと管理者'
      } else {
        this.roleId = ROLES.USER.ID
        this.userName = 'ユーザー'
      }
    },
  },
})

2つのactionについて説明します。
clearUser()で this.$reset() を実行し、Stateの初期化を行います。
setUser()でユーザー情報をStateに追加しており、isAdminをtrue/false切り替えることで
ログイン時、ユーザーが管理者なのか、一般ユーザーなのかを切り替えています。
そして、roleIdに定数化した権限のIDを指定しています。

ログイン用トークンをPiniaで保持(authストア)
~/store/auth.tsを作成し、以下のように記述します。

import { useUserStore } from './user'

interface State {
  isLogin: boolean
}

/**
 * リロード時のログイン状態判定
 */
const isDefaultLogin = () => {
  const token = useCookie('token', {
    secure: true,
    sameSite: 'strict',
  })
  return !!token.value
}

/**
 * ログイン状態
 */
export const useAuthStore = defineStore('auth', {
  state: (): State => {
    return {
      isLogin: isDefaultLogin(),
    }
  },
  actions: {
    /**
     * ログイン
     */
    login() {
      const token = useCookie('token', {
        maxAge: 60 * 60, // 有効期限を1時間
      })
      token.value = 'hogehogehoge'
      this.isLogin = true

      // ダミーユーザー情報をセット
      const { setUser } = useUserStore()

      setUser()
    },
    /**
     * ログアウト
     */
    logout() {
      const token = useCookie('token')
      token.value = null
      this.isLogin = false

      const { clearUser } = useUserStore()
      clearUser()
    },
  },
})

authストアの初期値で、isDefaultLoginを実行しています。
isDefaultLoginでtokenという名前のCookieが存在しているかを判定しています。
Piniaでは永続化ライブラリがあると思いますが、有効期限などが設定できないため、
今回はNuxt3標準のコンポーザブル機能のuseCookieを使い、
リロードしても状態を保持できるようにしています。
また、actionについて
login()は、疑似ログイン機能のメイン部分です。
useCookieを使い、token 名でCookieを保持するようにしています。
そしてuserストアのsetUser()を実行しユーザー情報をPiniaで保持します。
logout()は、疑似ログアウト機能のメイン部分です。
useCookieを使い、token をnullにすることでCookieを削除しています。
そしてuserストアのclearUser()を実行しユーザー情報を初期化します。

ストアの呼び出し
ログインボタン・ログアウトボタンのクリック時に、
authストアのlogin(), logout()を呼び出します。
~/pages/login.vueのscript部分にストア処理を追加します。

<script setup lang="ts">
import { useAuthStore } from '~/store/auth'

const authStore = useAuthStore()

const handleLoginClick = () => {
  authStore.login()
  navigateTo('/')
}
</script>

<template>
  <h1>ログイン</h1>
  <button type="button" @click="handleLoginClick()">ログイン</button>
</template>

ログインボタンのクリックイベント時に、authStoreのlogin()アクションを実行させています。

続いて、ログイン後のページ
~/pages/index.vue を以下のようにします。

<script setup lang="ts">
import { useAuthStore } from '~/store/auth'
import { useUserStore } from '~/store/user'

const authStore = useAuthStore()
const userStore = useUserStore()

const isLogin = computed(() => authStore.isLogin)

const handleLogoutClick = () => {
  authStore.logout()
  navigateTo('/login')
}
</script>

<template>
  <h1>管理画面トップ</h1>

  <div v-if="isLogin">
    <p>ようこそ、{{ userStore.userName }}さん</p>
    <button type="button" @click="handleLogoutClick">ログアウト</button>
  </div>
</template>

authStore, userStoreを呼び出しています。
ログアウトボタンのクリックイベント時に、authStoreのlogout()アクションの実行させています。
authStoreのログイン状態のStateを使用し、ログイン時に表示するコンテンツの切り替えを行っています。
ログイン時に表示するコンテンツのユーザー情報はusetStoreから取得しています。

動作確認

ここまで実装した機能について、
実際にログイン・ログアウトボタンをクリックして確認してみたいと思います。

上記キャプチャのように動作していればokです!

前編のまとめ

ここまでで以下の機能が実装できました!

  • ログインボタンクリック時にtokenが発行され、Cookieに保存される(ログイン)
  • ログアウトボタンクリック時にCookieに保存していたtokenが破棄される(ログアウト)
  • ~/store/user.tsのsetUser()で管理者/一般ユーザー 権限の切り替えができる(権限の切り替え)
  • リロードしてもCookieの値からログイン中かどうかを判別する(ログイン情報の保持)

後編では、ページごとに権限を付与する方法やアクセス時の権限を制御する方法についてご紹介します!!

前編で実装したコードについてはこちらにあります!!
https://github.com/p-t-a-p-1/nuxt3-control-page-access/tree/chapter-1

RELATED ARTICLE