🔒

Nuxt 3 の Route Middleware で簡単な認証フローを構築する

2022/01/27に公開

Nuxt 3 にも middleware (Route Middleware) が実装されました。

https://v3.nuxtjs.org/docs/directory-structure/middleware

Route Middleware は、サーバーサイド・クライアントサイド共、ルーティング(ページ遷移)にあたって行う共通処理を記述することができます。

たとえば、ログインが必要なページに対し、ログイン中かどうかを判定し、必要に応じてログインページを表示するなどの場合に使用します。

Composables を使用すれば同等の処理を行うことも可能ですが、より簡潔に、宣言的に(Page コンポーネントに)記述することが可能になります。

以下、とくに言及がない場合は Route Middleware を指します。

Route Middleware の基本的な使い方

Nuxt 2 までにも存在していた middleware と基本的な使い方は同様です。
Page コンポーネントや Layout コンポーネントにて、使用する middleware を指定すると、コンポーネントが作成 (setup) される前に(ページ遷移に先立ち) middleware 内の指定のファイルが実行されます。

Nuxt 3 ではファイル名を foo.global.ts のように .global をつけることで、すべてのページに適用される middleware を作成することが可能です(nuxt.config.ts に記述する必要はありません)

Page コンポーネント等では definePageMeta() を使用します。

pages/mypage.vue
<script setup lang="ts">
definePageMeta({
  middleware: ['auth'], // もしくは 'auth'
  // middleware: 'auth',
})
</script>

defineNuxtRouteMiddleware()

middleware は middleware/foo.ts のように作成します。

middleware/foo.ts
export default defineNuxtRouteMiddleware((to, from) => {
  console.log('middleware')
})

上述のとおりファイル名の suffix として .global をつけることで、すべてのページで実行される共通の middleware を実装できます。

middleware/foo.global.ts
export default defineNuxtRouteMiddleware((to, from) => {
  console.log('common middleware')
})

リダイレクトする場合

必要に応じて別のページに遷移させる場合は、遷移先を return します。

middleware/foo.ts
export default defineNuxtRouteMiddleware((to, from) => {
  return { path: '/login' }
  // return '/login'
})

遷移先の記述は <nuxt-link>to プロパティに与えるものと同様です。

ヘルパー関数 navigateTo() が用意されていて middleware 以外に Plugin や Page コンポーネント等でも利用できます。

middleware/foo.ts
export default defineNuxtRouteMiddleware((to, from) => {
  return navigateTo('/')
  // return navigateTo({ path: '/' })
})

遷移しない場合

何らかの条件で遷移させないケースでは return false で遷移させないことができます。

middleware/foo.ts
export default defineNuxtRouteMiddleware((to, from) => {
  return false
})

abortNavigation() を使用する(推奨)

ヘルパー関数 abortNavigation() を利用すると開発環境 (process.dev) で Errorthrow することができます。

下記のコードは上記と同一です。

middleware/foo.ts
export default defineNuxtRouteMiddleware((to, from) => {
  return abortNavigation()
  // return abortNavigation('ナビゲーションガード')
})

認証フローで middleware を使用する例

もっとも一般的な利用方法のひとつが、ログインが必要なページに対する共通処理でしょう。

Composables とともに利用する場合は、たとえば次のようになります。

composables/useAuth.ts
import type { Ref } from 'vue'

const login = (currentUser: Ref<boolean>) =>  async () => {
  currentUser.value = true
  // middleware でログインページにリダイレクトした場合は redirectFrom に元のページが入っている
  const to = useRoute().redirectedFrom?.path || '/'
  useRouter().push(to)
}

const logout = (currentUser: Ref<boolean>) => async () => {
  currentUser.value = false
}

export const useAuth = () => {
  const currentUser = useState('currentUser', () => false)
  return {
    currentUser,
    login: login(currentUser),
    logout: logout(currentUser),
  }
}

これを middleware で読み込みます。

middleware/auth.ts
import { useAuth } from '@/composables/useAuth'

export default defineNuxtRouteMiddleware((to, from) => {
  // この middleware が設定されている場合は要ログイン
  const { currentUser } = useAuth()
  if (!currentUser.value && to.path !== '/login') {
    const path = '/login'
    return { path }
  }
})

認証が必要なページで middleware を読み込みます。

pages/mypage.vue
<script setup lang="ts">
definePageMeta({
  middleware: 'auth',
})
</script>

<template>
  <div>
    <h1>🔒 マイページ</h1>
  </div>
</template>

最後に、ログインページを用意します。

pages/login.vue
<script setup lang="ts">
const { currentUser, login } = useAuth()

const userLogin = async () => {
  await login()
}
</script>

<template>
  <div>
    <button @click="userLogin">ログイン</button>
  </div>
</template>

たとえば index.vue から mypage.vue へ遷移する場合は
index.vue → (mypage.vue →) login.vueClick [ログイン]mypage.vue
と遷移します。

その他対応が必要なもの

middleware の利用例から外れるので詳解しませんが、カスタマイズが必要なものとしては次のようなものがあるでしょう。

ローディングアイコン

ログイン時はDBアクセス等により少なからず時間がかかるので、ローディングアイコンを表示するなどの対処が必要になりそうです。

ログアウト後の処理

いまのままでは、要ログインページでログアウトした場合に、そのページが表示され続けます。

リロード時の処理

要ログインページに直接アクセスした場合やリロードを行った場合に、ログインページへのリダイレクトが行われてしまいます。

SessionStorage 等を利用しログイン状態を維持するなどの対応が必要になります。

Firebase Auth のようなクライアントサイドだけで行うログインフロー

Plugin の .client.ts を使用して、クライアント側だけで Firebase Auth 等を利用すると良さそうです。

Nuxt 3 は Plugin がプロミスを返す場合、その解決がされるまで以降の処理に進みません。
middleware がある場合も Plugin を適切に await した後 middleware の処理に進みます。

ただし、初回に表示されるページについてはハイドレートがミスマッチにならないように気をつける必要がありそうです。

補足 : ライフサイクル(実行順序)

次のファイルがある場合の実行順序は次のとおりです。

  1. plugins/bar.server.ts サーバー側のレンダリング時に1度だけ
  2. plugins/baz.client.ts クライアント側のレンダリング時に1度だけ
  3. middleware/foo.ts Page コンポーネントの読み込みごと(ページ遷移の前)
  4. Page Component: setup <script setup> 等

まとめ

以上、簡単ではありますが Nuxt 3 に実装されたばかりの Route Middleware の記事を書きました(2022年1月27日)

この記事が、誰かの一助になれば幸いです。

書いた内容は執筆時点の理解のため、間違いや誤解を招く表現等が混ざっている可能性があります。
正確な記述に直したいと思いますので、気づいた方は修正内容をコメント等で教えてもらえますでしょうか。

Discussion