Nuxtのミドルウェア認証とは、ページを開く前に動く「ルートミドルウェア」でログイン状態を確かめ、未ログインならログイン画面へ移動させる仕組みです。ただしルートミドルウェアが守るのは「画面」だけなので、APIはサーバー側(requireUserSession)でも必ず守ります。 この2段構えが、Nuxtで認証を作るときの基本形です。

この記事では、Nuxt 4.5.2 と nuxt-auth-utils 0.5.30 を使い、メールアドレスとパスワードでログインする最小構成をそのまま貼って動くコードで紹介します。さらにPlaywright(ブラウザ自動操作ツール)とcurlで「ログイン前→ログイン→保護ページ→ログアウト」を動かし、302・401・Cookieの属性(HttpOnly・Secure・SameSite・暗号化)を実測しました。無限リダイレクトなどのよくある失敗も再現しています。

検証環境:Node.js 22.22.2 / Nuxt 4.5.2 / Nitro 2.13.4 / Vue 3.5.43 / vue-router 5.3.1 / nuxt-auth-utils 0.5.30 / Playwright 1.56.1(Chromium)

Nuxtの認証ミドルウェア早見表

種類 置き場所・書き方 いつ動くか どこで動くか 認証での用途
名前付きルートミドルウェア app/middleware/auth.ts を作り、ページで definePageMeta({ middleware: 'auth' }) 指定したページへの遷移時 初回表示はサーバー+クライアント、以降の画面遷移はクライアント ログイン必須ページの保護(基本はこれ)
グローバルルートミドルウェア app/middleware/xxx.global.ts すべての画面遷移 同上 サイト全体を原則ログイン必須にする、ログ
インラインミドルウェア definePageMeta({ middleware: [ (to, from) => { … } ] }) そのページへの遷移時 同上 そのページだけの追加条件(権限など)
サーバーミドルウェア server/middleware/log.ts すべてのHTTPリクエスト(API・ページ・静的以外) サーバーのみ ログ、ヘッダー付与。画面遷移では動かない
requireUserSession(event) server/api/** の中で呼ぶ そのAPIが呼ばれたとき サーバーのみ APIの保護(未ログインは401)

本記事の内容は、ご自由にお使いください。
ご利用の際は、出典として本ページへのリンクを記載いただけますようお願いします。

(記載例)出典:株式会社RJC「Nuxtのmiddlewareで認証|nuxt-auth-utilsで実装し実測」

ミドルウェアの実行順と実行場所を理解する

ルートミドルウェアは「グローバル(ファイル名のアルファベット順)→ ページで指定した順」に実行されます。 順番を固定したいときは 01.log.global.ts、02.auth.global.ts のように番号を付けます(公式ドキュメントの推奨)。

もう1つ大事なのが実行場所です。Nuxtは最初のアクセスをサーバーで描画(SSR)するため、初回表示のときはミドルウェアがサーバーで1回、ブラウザでの読み込み(ハイドレーション)時にもう1回動きます。 2回目以降の画面遷移はブラウザだけで動きます。

sequenceDiagram
    participant B as ブラウザ
    participant S as Nuxtサーバー
    B->>S: GET /dashboard(初回)
    S->>S: server/middleware → ルートミドルウェア
    S-->>B: 未ログインなら302、ログイン済みなら200 HTML
    B->>B: ハイドレーション時にルートミドルウェア
    B->>B: 以降の画面遷移はクライアントだけで実行

どこで動いたかをログに出して確かめました。

// app/middleware/01.log.global.ts(すべての画面遷移で動くグローバルミドルウェア)
export default defineNuxtRouteMiddleware((to, from) => {
  const where = import.meta.server ? 'server' : 'client'
  console.log(`[route:${where}] ${from.fullPath} -> ${to.fullPath}`)
})
// server/middleware/log.ts(サーバーミドルウェア:すべてのリクエストで動く)
export default defineEventHandler((event) => {
  console.log(`[server-mw] ${event.method} ${getRequestURL(event).pathname}`)
})

未ログインで /dashboard にアクセスしたときのサーバーログ(実行結果):

[server-mw] GET /dashboard
[server-mw] GET /api/_auth/session
[route:server] /dashboard -> /dashboard
[route:server] /dashboard -> /login?redirect=/dashboard

サーバーミドルウェアが先に動き、その後でルートミドルウェアがサーバー上で動いていることがわかります。/api/_auth/session は nuxt-auth-utils の内部リクエストです。ブラウザのコンソールにはハイドレーション時の [route:client] … も出ました。

ミドルウェア内では useRoute() ではなく引数の to / from を使うのが公式の推奨です。ハイドレーション時の2回目を省きたい場合は nuxtApp.isHydrating && nuxtApp.payload.serverRendered で判定できます。

完成形のファイル構成

ここから、メール・パスワードでログインする完成形を作ります。Nuxt 4の構成で、app/(画面とルートミドルウェア)、server/(APIとユーザー表)、shared/(型)に分けて置きます。

手順1:nuxt-auth-utilsを入れてセッション用の鍵を用意する

npx nuxi@latest module add auth-utils
# または
npm i nuxt-auth-utils@0.5.30
// nuxt.config.ts
export default defineNuxtConfig({
  compatibilityDate: '2026-09-01',
  modules: ['nuxt-auth-utils'],
  runtimeConfig: {
    // nuxt-auth-utils のセッション設定(任意)
    session: {
      maxAge: 60 * 60 * 24 * 7, // 7日。指定しないとブラウザを閉じると消えるCookieになる
      cookie: { secure: true },
    },
  },
})

セッションはCookieの中に暗号化して保存されます(サーバーにセッション表を持たない方式)。暗号化の鍵が NUXT_SESSION_PASSWORD です。

nuxt dev では未設定なら自動生成されて .env に追記されますが(実測)、本番(node .output/server/index.mjs)では自分で設定が必要です。

本番で未設定のままログインしたときのサーバーログ(実行結果):

[nuxt-auth-utils] NUXT_SESSION_PASSWORD environment variable or runtimeConfig.session.password was not set.
[request error] [unhandled] [POST] http://localhost:7460/api/auth/login
 H3Error: Empty password

短すぎる値(short-password)を設定したとき:

H3Error: Password string too short (min 32 characters required)

32文字以上のランダムな文字列を、本番のサーバーの環境変数に設定します。 次のコマンドで64文字の値を作れます。

node -e "console.log(require('crypto').randomBytes(32).toString('hex'))"

手順2:ユーザー表とパスワードのハッシュ化

パスワードは平文で保存せず、nuxt-auth-utils の hashPassword でハッシュ化(元に戻せない形に変換)して保存し、ログイン時に verifyPassword で照合します。内部では scrypt という計算コストの高いハッシュ方式が使われます。

// shared/types/auth.d.ts(セッションに入れるユーザーの型)
declare module '#auth-utils' {
  interface User {
    id: number
    name: string
    email: string
  }
}
export {}
// server/utils/users.ts(検証用のメモリ上のユーザー表。本番はDBに置き換える)
type UserRecord = { id: number, name: string, email: string, passwordHash: string }

let users: UserRecord[] | null = null

export async function findUserByEmail(email: string) {
  if (!users) {
    // 起動後の最初の呼び出しでハッシュ化して用意する(平文は保存しない)
    users = [
      { id: 1, name: '山田花子', email: 'hanako@example.com', passwordHash: await hashPassword('password123') },
    ]
  }
  return users.find(u => u.email === email.toLowerCase()) ?? null
}

server/utils/ の関数と hashPassword / verifyPassword は server/ の中で自動インポートされるため、import 文は不要です。

同じ password123 を2回ハッシュ化した結果(nuxt-auth-utils が内部で使う @adonisjs/hash の scrypt ドライバーを既定設定で直接呼んだ実行結果):

$scrypt$n=16384,r=8,p=1$Laeo28yE86R2BI1AuMo3nw$6rESlc91Lf8aH8OT0KOISMxNxzMo6IoEAGV7viW7/J+vlBMSCc0aVEidfYbgTvMFFceKA0CLZ8Y9jJqRXNeMpw
$scrypt$n=16384,r=8,p=1$MJcZ3DSr4c8N02DBL3OGQg$LQ+XJ1JX4kSvFkDJLyIEoenswq/JbPpApt/HKcA75ZbvGJkCrYFBFb7/ZdCIZzTB6JqexFu7m0zeyy2F4OjNOw
same? false verify: true false

毎回ランダムなソルト(混ぜ込む値)が入るので、同じパスワードでも結果は別の文字列になります。 DBにはこの文字列をそのまま保存し、findUserByEmail の中身だけをDB検索に置き換えます。

手順3:ログインAPIとログアウトAPIを作る

// server/api/auth/login.post.ts
export default defineEventHandler(async (event) => {
  const { email, password } = await readBody<{ email?: string, password?: string }>(event)
  if (!email || !password) {
    throw createError({ statusCode: 400, statusMessage: 'email と password は必須です' })
  }

  const user = await findUserByEmail(email)
  // ユーザーがいない場合も同じエラーにして、登録済みかどうかを推測されにくくする
  if (!user || !(await verifyPassword(user.passwordHash, password))) {
    throw createError({ statusCode: 401, statusMessage: 'Unauthorized' })
  }

  await setUserSession(event, {
    user: { id: user.id, name: user.name, email: user.email },
    loggedInAt: Date.now(),
  })
  return { ok: true }
})
// server/api/auth/logout.post.ts(useUserSession().clear() を使うなら無くても可)
export default defineEventHandler(async (event) => {
  await clearUserSession(event)
  return { ok: true }
})
サーバー関数 何をするか
setUserSession(event, data) セッションに追記(マージ)して暗号化Cookieを返す
replaceUserSession(event, data) セッションを丸ごと置き換える
getUserSession(event) 現在のセッションを読む(未ログインでもエラーにしない)
requireUserSession(event) ログイン必須。未ログインなら401を投げる
clearUserSession(event) セッションを消す(ログアウト)

セッションには表示に必要な最小限(id・名前など)だけを入れます。 Cookieにはサイズの上限があり、毎リクエスト送られるためです。

手順4:ページを守る(名前付きミドルウェア)

// app/middleware/auth.ts(名前付きミドルウェア)
export default defineNuxtRouteMiddleware((to) => {
  const { loggedIn } = useUserSession()

  if (!loggedIn.value) {
    // 元のページを覚えておき、ログイン後に戻す
    return navigateTo({ path: '/login', query: { redirect: to.fullPath } })
  }
})
// app/middleware/guest-only.ts(ログイン済みならログイン画面を見せない)
export default defineNuxtRouteMiddleware(() => {
  const { loggedIn } = useUserSession()
  if (loggedIn.value) {
    return navigateTo('/dashboard')
  }
})

navigateTo は必ず return します。 付けないと、移動の指示の後も後続の処理が進みます。

useUserSession() で使える値は次のとおりです。

値・関数 内容
loggedIn ログイン中なら true(computed)
user setUserSession で入れた user
fetch() サーバーからセッションを読み直す(ログイン直後に呼ぶ)
clear() /api/_auth/session にDELETEを送りログアウト

既定(loadStrategy: 'server-first')ではSSR時にサーバーでCookieを読んでセッションを用意するため、サーバーで動くルートミドルウェアでも loggedIn を判定できます。

手順5:APIを守る(requireUserSession)

// server/api/todos/index.get.ts(ログインした人だけが使えるAPI)
export default defineEventHandler(async (event) => {
  const { user } = await requireUserSession(event) // 未ログインなら 401
  return [
    { id: 1, title: `${user.name} さんのタスク:備品の発注` },
    { id: 2, title: `${user.name} さんのタスク:週報を書く` },
  ]
})

ルートミドルウェアは画面遷移でしか動かないため、APIは直接呼ばれると素通りします。 APIは requireUserSession で守ります。まとめて守りたいときは、server/middleware/ で event.path を見て requireUserSession を呼ぶ方法もあります。

手順6:ログイン画面と保護された画面

<!-- app/pages/login.vue -->
<script setup lang="ts">
definePageMeta({ middleware: 'guest-only' })

const { fetch: refreshSession } = useUserSession()
const route = useRoute()
const form = reactive({ email: '', password: '' })
const errorMessage = ref('')

async function login() {
  errorMessage.value = ''
  try {
    await $fetch('/api/auth/login', { method: 'POST', body: form })
    await refreshSession() // Cookie の中身をクライアントの状態に反映
    const redirect = typeof route.query.redirect === 'string' ? route.query.redirect : '/dashboard'
    await navigateTo(redirect.startsWith('/') ? redirect : '/dashboard')
  } catch {
    errorMessage.value = 'メールアドレスまたはパスワードが違います'
  }
}
</script>

<template>
  <main>
    <h1>ログイン</h1>
    <form @submit.prevent="login">
      <input v-model="form.email" type="email" name="email" placeholder="メールアドレス" required>
      <input v-model="form.password" type="password" name="password" placeholder="パスワード" required>
      <button type="submit">ログイン</button>
    </form>
    <p v-if="errorMessage" role="alert">{{ errorMessage }}</p>
  </main>
</template>
<!-- app/pages/dashboard.vue -->
<script setup lang="ts">
definePageMeta({ middleware: 'auth' })

const { user, clear } = useUserSession()
// SSR 中も useFetch ならブラウザの Cookie が API に転送される
const { data: todos } = await useFetch('/api/todos')

async function logout() {
  await clear() // /api/_auth/session に DELETE を送り、Cookie を消す
  await navigateTo('/login')
}
</script>

<template>
  <main>
    <h1>ダッシュボード(ログインした人だけ)</h1>
    <p>ようこそ、{{ user?.name }} さん</p>
    <ul>
      <li v-for="t in todos" :key="t.id">{{ t.title }}</li>
    </ul>
    <button @click="logout">ログアウト</button>
  </main>
</template>

ポイントは2つです。 ログイン直後に fetch()(ここでは refreshSession)を呼んで、クライアント側の loggedIn を更新すること。そして redirect クエリは / で始まるものだけ受け付け、外部サイトへの移動(オープンリダイレクト)に使われないようにすることです。

動作確認1:Playwrightでログインからログアウトまで

nuxt build 後に node .output/server/index.mjs で起動し、Playwrightで操作しました。

// auth.mjs(node auth.mjs で実行。playwright 1.56.1)
import { chromium } from 'playwright'
const BASE = 'http://localhost:7460'
const browser = await chromium.launch()
const page = await browser.newPage()
const path = () => page.url().replace(BASE, '')

await page.goto(`${BASE}/dashboard`)
console.log('1. 未ログインで /dashboard ->', path())
await page.fill('input[name=email]', 'hanako@example.com')
await page.fill('input[name=password]', 'wrong-password')
await page.click('button[type=submit]')
console.log('2. 間違ったパスワード ->', await page.textContent('[role=alert]'))

await page.fill('input[name=password]', 'password123')
await page.click('button[type=submit]')
await page.waitForURL('**/dashboard')
await page.waitForSelector('li')
console.log('3. ログイン後 ->', path(), '/', await page.textContent('h1'))
console.log('   タスク:', await page.$$eval('li', els => els.map(e => e.textContent)))

const c = (await page.context().cookies()).find(c => c.name === 'nuxt-session')
console.log('4. Cookie:', { httpOnly: c.httpOnly, secure: c.secure, sameSite: c.sameSite, valueHead: c.value.slice(0, 10) + '…', length: c.value.length })
console.log('   document.cookie から見える? ->', JSON.stringify(await page.evaluate(() => document.cookie)))

await page.goto(`${BASE}/login`)
console.log('5. ログイン中に /login ->', path())
await page.click('text=ログアウト')
await page.waitForURL('**/login')
console.log('6. ログアウト後 ->', path())
await page.goto(`${BASE}/dashboard`)
console.log('7. もう一度 /dashboard ->', path())
console.log('8. ログアウト後の /api/todos ->', (await page.request.get(`${BASE}/api/todos`)).status())
await browser.close()

実行結果:

1. 未ログインで /dashboard -> /login?redirect=/dashboard
2. 間違ったパスワード -> メールアドレスまたはパスワードが違います
3. ログイン後 -> /dashboard / ダッシュボード(ログインした人だけ)
   タスク: [ '山田花子 さんのタスク:備品の発注', '山田花子 さんのタスク:週報を書く' ]
4. Cookie: {
  httpOnly: true,
  secure: true,
  sameSite: 'Lax',
  valueHead: 'Fe26.2**58…',
  length: 454
}
   document.cookie から見える? -> ""
5. ログイン中に /login -> /dashboard
6. ログアウト後 -> /login
7. もう一度 /dashboard -> /login?redirect=/dashboard
8. ログアウト後の /api/todos -> 401

未ログイン→リダイレクト、ログイン後に元のページへ戻る、ログイン中はログイン画面を飛ばす、ログアウト後はページもAPIも閉じる、の4点がすべて期待どおりに動きました。

動作確認2:curlで401と302を確かめる

# 未ログインで保護API
curl -s -i http://localhost:7460/api/todos
HTTP/1.1 401 Server Error
set-cookie: nuxt-session=Fe26.2**c5c9…; Path=/; HttpOnly; Secure; SameSite=Lax
content-type: application/json
…
{
  "error": true,
  "url": "http://localhost:7460/api/todos",
  "statusCode": 401,
  "statusMessage": "Server Error",
  "message": "Unauthorized"
}
# 未ログインで保護ページ
curl -s -i http://localhost:7460/dashboard
HTTP/1.1 302 Found
location: /login?redirect=/dashboard

ログイン後のCookieを curl -b で付けると、タスク一覧が返りました。ページ(SSR)はサーバーで302を返すため、JavaScriptを実行しないクライアントにも保護ページの中身は渡りません。 APIは requireUserSession により401が返ります(statusMessage は Server Error と表示されます)。

Cookieの属性を実測する

Playwrightとcurlで確認した nuxt-session Cookie の属性です。

属性 実測値 意味
値 Fe26.2**…(454文字) iron形式で暗号化・署名された値。中身のユーザー情報は読めず、改ざんすると無効になる
HttpOnly あり JavaScript(document.cookie)から読めない。実測でも ""
Secure あり(本番ビルド) HTTPSでのみ送信。localhostは例外的にHTTPでも送られる
SameSite Lax 他サイトからのPOSTなどではCookieを送らない
Expires maxAge 未指定なら無し 指定するとExpiresが付く(maxAge: 604800 で7日後の日時を確認)

maxAge を設定したときの応答ヘッダー(実行結果):

set-cookie: nuxt-session=…; Path=/; Expires=Tue, 06 Oct 2026 10:41:12 GMT; HttpOnly; Secure; SameSite=Lax

navigateToとabortNavigationの使い分け

やりたいこと 書き方 実測した結果
未ログインをログイン画面へ return navigateTo('/login') SSR:302 / クライアント:画面が切り替わる
元のURLを覚えて移動 return navigateTo({ path: '/login', query: { redirect: to.fullPath } }) location: /login?redirect=/dashboard
恒久的な移動 return navigateTo('/new', { redirectCode: 301 }) 301
権限がないので止める return abortNavigation(createError({ statusCode: 403, statusMessage: 'Forbidden' })) SSR:403のエラーページ / クライアント:遷移が中止され元の画面のまま

「ログインすれば見られる」ならnavigateTo、「ログインしても権限がない」ならabortNavigation、と使い分けるとわかりやすくなります。インラインミドルウェアと名前付きミドルウェアを組み合わせた例です。

<!-- app/pages/admin.vue -->
<script setup lang="ts">
definePageMeta({
  middleware: [
    'auth', // 名前付き:まずログインを確認
    // インライン:このページだけの追加チェック
    function (to, from) {
      const { user } = useUserSession()
      if (user.value?.email !== 'admin@example.com') {
        return abortNavigation(createError({ statusCode: 403, statusMessage: 'Forbidden' }))
      }
    },
  ],
})
</script>

<template><main><h1>管理画面</h1></main></template>

実行結果:

状態 アクセス方法 結果
未ログイン curlで /admin 302 → /login?redirect=/admin(auth が先に動く)
一般ユーザーでログイン curlで /admin HTTP/1.1 403 Forbidden
一般ユーザーでログイン ブラウザで router.push('/admin') Promiseが Forbidden で reject され、トップ画面のまま

配列に書いた順に実行されるので、ログイン確認→権限確認の順に並べます。API側にも同じ権限チェックを入れます。

よくある失敗と直し方

失敗 起きること(実測) 直し方
クライアントだけでチェック 保護ページのHTMLが200 サーバーでもチェック
ログイン画面自体を保護 サーバーでミドルウェアが回り続け応答なし ログイン画面を除外
APIを守っていない ページは302でもAPIは読める requireUserSession
HTTP(localhost以外)で確認 ログイン200でもCookieが保存されない HTTPSで確認

失敗1:クライアントでだけチェックしている

// ✗ 直す前:app/middleware/auth-client-only.ts
export default defineNuxtRouteMiddleware(() => {
  if (import.meta.server) return // ← サーバーでは素通り
  const { loggedIn } = useUserSession()
  if (!loggedIn.value) return navigateTo('/login')
})

このミドルウェアを付けたページに、未ログインのままcurlでアクセスした結果:

HTTP/1.1 200 OK
…<main><h1>社外秘の備品リスト</h1><p>SSRのHTMLに入ってしまう文言</p></main>…

ブラウザではすぐログイン画面へ移るので気づきにくいのですが、サーバーが描画したHTMLに保護したい内容が入っています。 if (import.meta.server) return を消し、手順4の auth.ts のようにサーバーでも判定すれば302になります。なお、nuxt generate(静的出力)では、このページは中身ごとHTMLファイルとして書き出されました。静的サイトではサーバー側のチェックが動かないため、ログインが必要なページはSSR(Nodeサーバー)で配信するのが確実です。

失敗2:無限リダイレクト

// ✗ 直す前:ログイン画面自体も「未ログインならログイン画面へ」の対象になっている
export default defineNuxtRouteMiddleware((to) => {
  if (!to.path.startsWith('/loop')) return
  const { loggedIn } = useUserSession()
  if (!loggedIn.value) return navigateTo('/loop/login')
})

グローバルミドルウェアでこう書き、未ログインで /loop/login にアクセスすると、サーバー内でリダイレクトが繰り返され、curlには応答が返りませんでした(タイムアウト)。サーバーログには同じ行が大量に出続け、十数秒で65万行を超えました。

[route:server] /loop/login -> /loop/login
[route:server] /loop/login -> /loop/login
[route:server] /loop/login -> /loop/login
…

このときNodeのプロセスはCPUを使い続け、SIGTERMでは止まらず強制終了が必要でした。遷移先が自分自身になる条件を必ず外します。

// ○ 直した後
export default defineNuxtRouteMiddleware((to) => {
  if (!to.path.startsWith('/loop')) return
  if (to.path === '/loop/login') return // ← ログイン画面は除外
  const { loggedIn } = useUserSession()
  if (!loggedIn.value) return navigateTo('/loop/login')
})

直した後は /loop/login が200、/loop が302になりました。グローバルミドルウェアで「原則ログイン必須」にするときは、除外するページ(ログイン、パスワード再設定、利用規約など)を配列で持つと安全です。

// app/middleware/02.auth.global.ts(サイト全体を原則ログイン必須にする例)
const publicPaths = ['/login', '/signup', '/password-reset']

export default defineNuxtRouteMiddleware((to) => {
  if (publicPaths.includes(to.path)) return
  const { loggedIn } = useUserSession()
  if (!loggedIn.value) {
    return navigateTo({ path: '/login', query: { redirect: to.fullPath } })
  }
})

失敗3:HTTPの検証環境でログインできない

本番ビルドのCookieには Secure が付くため、http://192.0.2.2:7460 のようにlocalhost以外をHTTPで開くと、ブラウザがCookieを保存しません。 Playwrightで確かめた結果です。

login status 200 set-cookie has Secure: true
url after login: /login?redirect=/dashboard
cookies stored: []

ログインAPIは成功(200)しているのに、次の画面でログイン画面へ戻されます。本番と同じくHTTPSで確認するのが基本です。社内の検証環境でどうしてもHTTPを使う場合は、nuxt.config.ts の runtimeConfig.session.cookie.secure を定義したうえで、その環境だけ NUXT_SESSION_COOKIE_SECURE=false を渡します。

# NUXT_SESSION_COOKIE_SECURE=false を渡したとき(実行結果)
set-cookie: nuxt-session=…; Path=/; Expires=…; HttpOnly; SameSite=Lax

nuxt.config.ts に cookie: { secure: true } を書かずに環境変数だけ渡した場合は、Secure は外れませんでした。環境変数で上書きできるのは、nuxt.config の runtimeConfig に定義したキーだけだからです。

よくある質問

Q. サーバーミドルウェア(server/middleware)だけで認証できますか? A. APIの保護には使えますが、NuxtLinkによる画面遷移では動きません。画面はルートミドルウェア、APIは requireUserSession(またはサーバーミドルウェア)と、役割を分けるのが基本です。

Q. ログインしたのに画面がログイン前の表示のままです。 A. ログインAPIを呼んだ後に useUserSession() の fetch() を呼んでいないと、ブラウザ側の loggedIn が更新されません。手順6のように await fetch() してから navigateTo します。

Q. セッションをサーバー側で強制的に無効化(全端末ログアウトなど)したいときは? A. セッションはCookie自体に中身を持つ方式なので、サーバー側だけで個別に無効化する仕組みは標準ではありません。必要なら、セッションに入れたIDをDBの有効なセッション一覧と照合する処理を requireUserSession の後に加えます。

まとめ

  • ルートミドルウェアは画面、requireUserSession はAPI。両方で守るのがNuxt認証の基本形です。
  • 種類は名前付き・.global・インライン・サーバーミドルウェアの4つ。実行順は「グローバル(名前順)→ページ指定順」です。
  • 初回表示ではサーバーとクライアントの両方でミドルウェアが動くため、サーバー側のチェックを抜かないことが重要です。
  • nuxt-auth-utils 0.5 では hashPassword / verifyPassword / setUserSession / useUserSession でメール・パスワードログインを短いコードで実装できます。
  • 実測では、未ログインのページは302、APIは401、CookieはHttpOnly・Secure・SameSite=Lax・暗号化でした。
  • ログイン画面の除外忘れは応答が返らない無限ループになります。除外リストを必ず用意します。
  • navigateTo は必ず return。権限不足は abortNavigation で403にできます。

本記事の内容は、ご自由にお使いください。
ご利用の際は、出典として本ページへのリンクを記載いただけますようお願いします。

(記載例)出典:株式会社RJC「Nuxtのmiddlewareで認証|nuxt-auth-utilsで実装し実測」

株式会社RJCでは、認証のように「動いた」だけで終わらせず、実際の応答やCookieまで確かめる姿勢を、プロジェクトの中で仲間と学び合っています。こうした知識を共有しながら、日々技術を磨いています。

株式会社RJC ― SI事業・SES事業・AI駆動開発。RJCは一緒に成長を楽しめる会社です。

WE ARE HIRING

RJCで一緒に開発しながら、
成長を楽しみませんか?

RJCは、Web・モバイル・AIを活用した開発プロジェクトで、テックリードやPM・PMOも活躍するシステム開発会社です。会社を知る、待遇を確かめる、話を聞いてみる。気になるところ見てみてください!

  • 127日年間休日
  • 12時間平均残業時間
  • 毎日ガチャ遊びココロも大切にする福利厚生。アマギフなどの賞品ラインナップ!

ほかにも、チケットレストラン、書籍読み放題、2年ごとの慰労報奨(休暇 or 金一封)、11期連続の黒字決算。

ABOUT RJC RJCがどんな会社か知る 考え方、研修、働き方、福利厚生、社員の前職まで。RJCのことが丸わかり! RJC丸わかりページへ JOB DESCRIPTION 仕事内容・待遇を見てみる 仕事内容、給与・待遇、選考の流れ。経験者も未経験も!応募前に知りたいこと、まとめました! 募集要項を見る ENTRY エントリーする エントリーは1〜2分・履歴書不要です。まずは話を聞いてみたい、という方でも歓迎です! エントリーフォームへ

RJCで一緒に開発しながら、 成長を楽しみませんか?