BLOG

ブログ

【Nuxt3】権限ごとのページアクセスの制御について(ページごと権限付与・ミドルウェア実装編)

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

ハコザキです。

本記事は、Nuxt3で権限ごとにページアクセスの制御機能を実装する記事の後編です。
前編をまだ読んでいない場合はぜひ前編から読んでいただければと思います..!!

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

前編で実装したコードについてはこちらにありますので、
後編だけ実装してみたい場合は、こちらをクローンしていただければと思います!
https://github.com/p-t-a-p-1/nuxt3-control-page-access/tree/chapter-1

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

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

本記事では、上記の2, 3を実装します。

前編のまとめ(ログインの仕組みについて)

前編では、以下の仕組みで非常に簡易的なログイン・ログアウト機能の実装、
ログイン情報の保持についてご紹介しました。

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

制御するページの作成

今回は3種ページを作成し、それぞれ制御させたいと思います。

  • /admin(管理者用トップページ)
    • 管理者のみアクセス可能な静的ページ
  • /admin/1, /admin/2(管理者用詳細ページ)
    • 管理者のみアクセス可能な動的ページ
  • /about(認証不要なページ)
    • ログイン不要な静的ページ(ログイン中もアクセス可)

実際にpagesディレクトリ内にVueファイルを作成します。

~/pages/admin/index.vue

<template>
  <h1>管理者限定ページ</h1>
  <ul>
    <li>
      <NuxtLink to="/admin/1">管理者1</NuxtLink>
    </li>
    <li>
      <NuxtLink to="/admin/2">管理者2</NuxtLink>
    </li>
    <li>
      <NuxtLink to="/admin/3">管理者3</NuxtLink>
    </li>
  </ul>
</template>

~/pages/admin/[id].vue

<script setup lang="ts">
const route = useRoute()
const id = route.params.id
</script>

<template>
  <h1>{{ id }} - 管理者限定ページ</h1>
</template>

~/pages/about.vue

<template>
  <h1>aboutページ ※認証不要</h1>
  <NuxtLink to="/">トップページへ戻る</NuxtLink>
</template>

静的ページの作成が完了しました。
作成したページに遷移するように、トップページ上にNuxtLinkを追加します。
~/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>
  <NuxtLink to="/admin">管理者限定ページ</NuxtLink>

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

続いてページごとに権限の設定をしていきます。

ページごとにアクセス可能な権限の設定

2つのステップで説明します。

  1. ページごとにアクセス可能な権限の設定
  2. ルーティングの拡張を行い、アクセス可能な権限情報の追加

はじめに、管理者のみアクセス可のページログインは必須だけど権限は問わないページログイン不要なページ など、
ページごとにアクセス可能な権限の設定を定数化して定義します。
続いて、Nuxtのルーティングシステムの機能を拡張し定数化した権限情報を各ルーティングに付与していきます。

ページ名ごとのアクセス可能な権限の定義

~/utils/constants.tsに以下を追記します。

...


/**
 * 権限によるページアクセス制限がある場合はここに追加
 * ※ 追加しない場合、ログイン中であればアクセス可能
 */
type PagePermissions = {
  // ルーティング名:権限ID[]
  [key: string]: number[]
}
// prettier-ignore
export const PAGE_PERMISSIONS: PagePermissions = {
  'admin': [ROLES.ADMIN.ID],
  'admin-id': [ROLES.ADMIN.ID],
  'user': [ROLES.USER.ID],
}

// 認証不要なページ
export const NO_PAGE_PERMISSIONS = ['login', 'about']

PAGE_PERMISSIONSという定数を定義しました。
中身は ルーティング名: 権限IDの配列のkey:valueの形のオブジェクトになっています。
ルーティング名はNuxtのpages配下で作成したVueファイルと紐づくようにします。
例えば、/adminの場合、pages/admin/index.vueを作成することでルーティングが生成されます。(pages/admin.vueでもok) この場合のルーティング名は’admin’となります。

また、 [id].vueなどの動的ルーティングの場合、-id のようにすることで紐づけることができます。 /admin/1/admin/2 の場合、pages/admin/[id].vueを作成します。 この場合のルーティング名は ‘admin-id’ となります。


認証が必要なルーティング名の設定とは別に、認証が不要なページ群も合わせて定義します。
今回はNO_PAGE_PERMISSIONSという定数で管理します。
loginページは認証が不要なので追加しております。
※ 権限による制御はしない でもログイン必須なページ の場合は、
PAGE_PERMISSIONS, NO_PAGE_PERMISSIONSどちらにも定義しないことに注意してください..!

ルーティング設定の拡張

続いて、上記で定義した権限設定を
Nuxtのルーティングを拡張し、それぞれ権限情報を付与していきたいと思います!
nuxt.config.tsを以下のように記述します。

import { PAGE_PERMISSIONS } from './utils/constants'
// https://nuxt.com/docs/api/configuration/nuxt-config
export default defineNuxtConfig({
  devtools: { enabled: true },

  modules: ['@pinia/nuxt'],

  hooks: {
    'pages:extend'(routes) {
      // ルーティングに権限IDを付与
      routes.forEach((route) => {
        if (!route.name) {
          return
        }
        const permissionRoleIds = PAGE_PERMISSIONS[route.name] || []
        route.meta = {
          ...route.meta,
          permissionRoleIds,
        }
      })
    },
  },
})

pages:extendhooksを使用し、
ルーティングごとに権限用の定数(PAGE_PERMISSIONS)のルーティング名(key)に対応する 権限ID配列(value)を、route.metaに追加しております。
route.name, route.metaをconsole.logで出力してみると以下のようになります。

route.name admin-id                                                                                                                                                                                                                                             6:44:14 PM
route.meta { permissionRoleIds: [ 1 ] }                                                                                                                                                                                                                         6:44:14 PM
---                                                                                                                                                                                                                                                             6:44:14 PM
route.name admin                                                                                                                                                                                                                                                6:44:14 PM
route.meta { permissionRoleIds: [ 1 ] }                                                                                                                                                                                                                         6:44:14 PM
---                                                                                                                                                                                                                                                             6:44:14 PM
route.name index                                                                                                                                                                                                                                                6:44:14 PM
route.meta { permissionRoleIds: [] }                                                                                                                                                                                                                            6:44:14 PM
---                                                                                                                                                                                                                                                             6:44:14 PM
route.name login                                                                                                                                                                                                                                                6:44:14 PM
route.meta { permissionRoleIds: [] }                                                                                                                                                                                                                            6:44:14 PM
---                                                                                                                                                                                                                                                             6:44:14 PM
route.name user                                                                                                                                                                                                                                                 6:44:14 PM
route.meta { permissionRoleIds: [ 2 ] }                                                                                                                                                                                                                         6:44:14 PM
---

このようにPAGE_PERMISSIONSの設定が反映されていればokです!

認証用・権限制御用のミドルウェアの作成

最後にミドルウェアを作成し、ページ遷移の際に遷移後のページのsetup関数が実行される前に 認証や権限チェックを行いたいと思います。

今回は2つのミドルウェアを作成したいと思います。

  1. 認証用のミドルウェア作成
  2. 権限制御用のミドルウェア作成

Nuxt3 middleware

今回は全ページに権限チェックを適用します。

その場合はファイル名に.globalをつけることで
全ページ遷移時にミドルウェアを実行することができます。
また、2つのミドルウェアのうち実行する順番を決めたい場合があると思います。
今回も認証のチェックを先に実行してから、
ログインユーザーの権限でアクセスできるページかどうかをチェックしたいです。
Nuxtのglobalなミドルウェアは、ファイル名のアルファベット順で実行するようになっています。 01.hoge.global.ts02.fuga.global.tsのように通し番号をつけることで実行順番を指定することができます。(公式と同じ形式になっております!!)

認証用ミドルウェアの作成

~/middleware/01.auth.global.tsを作成し以下のように記述します。

import { useUserStore } from '~/store/user'
import { NO_PAGE_PERMISSIONS } from '~/utils/constants'

/**
 * 認証用ミドルウェア
 * 未ログイン状態の場合、ログイン画面へリダイレクトする
 */
export default defineNuxtRouteMiddleware((to) => {
  const token = useCookie('token', {
    maxAge: 60 * 60,
  })

  const isNoPagePermission = NO_PAGE_PERMISSIONS.includes(to.name as string)
  // 認証不要なページの場合は何もしない
  if (isNoPagePermission) {
		token.value = token.value
    return
  }

  // トークンが存在しない場合はログインページへ
  if (!token.value) {
    return navigateTo('/login')
  }

  // ログインユーザーの権限ID
  const { roleId, setUser } = useUserStore()
  if (roleId === null) {
    // リロード時にストアが初期化されている場合は再取得
    setUser()
  }

  // Cookieの有効期限の更新
  token.value = token.value
})

このミドルウェアでは、以下の処理を行っております。

  • トークンのチェックでログイン済みかどうか
  • 認証不要なページの場合はスキップ
  • トークンはあるが権限情報がない場合
    • リロード時などでPiniaで管理していたユーザー情報が消えたときなど ※ Cookie上であまり複数情報を保持したくないため、今回はトークンのみCookie管理します
  • 認証済みの場合はCookieの有効期限を更新してトークンの値を再代入

権限制御用ミドルウェアの作成

続いて、~/middleware/02.user.global.tsを作成し以下のように記述します。

import { useUserStore } from '~/store/user'
import { NO_PAGE_PERMISSIONS } from '~/utils/constants'

export default defineNuxtRouteMiddleware((to) => {
  const { roleId } = useUserStore()

  const isNoPagePermission = NO_PAGE_PERMISSIONS.includes(to.name as string)
  if (isNoPagePermission) {
    return
  }

  if (roleId === null) {
    return navigateTo('/login')
  }

  const permissionRoleIds = to.meta.permissionRoleIds as number[]

  if (permissionRoleIds && permissionRoleIds.length) {
    if (!permissionRoleIds.includes(roleId)) {
      return showError(
        createError({
          statusCode: 403,
          message: 'アクセス権限がありません',
        })
      )
    }
  }
})

このミドルウェアでは、以下の処理を行っております。

  • 権限がない場合はログインページへ遷移
  • 遷移後の権限情報にログインユーザーの権限IDが存在しない場合はエラーページの表示

エラーページの作成

最後に権限エラーの場合のエラー表示など、カスタムエラーページを作成します。
~/error.vue(app.vueと同じ階層)を作成し以下のように記述します。

<script setup lang="ts">
const error = useError()
</script>

<template>
  <div>
    <p>{{ error?.message }}</p>
    <NuxtLink to="/">トップページへ戻る</NuxtLink>
  </div>
</template>

動作確認

このように、一般ユーザー権限でログインし、
/admin へアクセスした際にエラーページへ表示されており、
管理者権限でログインした際に、/adminへアクセスした際にページが表示されていればokです!
また、ログイン中でなくても/aboutへアクセスした際にページが表示されていればokです!

権限がないページへアクセスできなくなる
権限がある場合ページにアクセスできる
認証不要なページのアクセスもok

まとめ

前後編にわけて、権限ごとにページアクセスの制御機能を実装例についてご紹介しました。

管理画面において、ログイン機能はもちろん必須ですが、権限による機能制御や ページのアクセス制御もよく実装するものだと思います。
Nuxtの場合、pages:extendフックでルーティングの情報を拡張することができるので こちらを活用して権限付与、ミドルウェアで権限のチェックを行いました。

今回前後編で実装したものについてはこちらにpushしております!
興味ある方はぜひ見てみてください!!

https://github.com/p-t-a-p-1/nuxt3-control-page-access/tree/main

RELATED ARTICLE