Сессии и аутентификация

Аутентификация — типичное требование веб-приложений. В этом рецепте — базовая регистрация пользователя и вход в full-stack приложении Nuxt.

Введение

Настроим аутентификацию в full-stack Nuxt с Nuxt Auth Utils: удобные утилиты для сессии на клиенте и сервере.

Модуль хранит сессию в подписанных и запечатанных cookie, отдельная БД для сессий не нужна.

Установка nuxt-auth-utils

Установите модуль nuxt-auth-utils через CLI Nuxt.

Terminal
npx nuxt module add auth-utils
Команда добавит nuxt-auth-utils в зависимости и пропишет модуль в nuxt.config.ts.

Так как nuxt-auth-utils использует запечатанные cookie, сессия шифруется секретом из переменной окружения NUXT_SESSION_PASSWORD.

Если переменная не задана, в режиме разработки она может быть добавлена в .env автоматически.
.env
NUXT_SESSION_PASSWORD=a-random-password-with-at-least-32-characters
Перед деплоем в production задайте эту переменную в окружении.

API-маршрут входа

Создадим простой API-маршрут входа по статическим данным.

Маршрут /api/login принимает POST с email и password в теле.

server/api/login.post.ts
import { z } from 'zod'

const bodySchema = z.object({
  email: z.email(),
  password: z.string().min(8),
})

export default defineEventHandler(async (event) => {
  const { email, password } = await readValidatedBody(event, bodySchema.parse)

  if (email === 'admin@admin.com' && password === 'iamtheadmin') {
    // set the user session in the cookie
    // this server util is auto-imported by the auth-utils module
    await setUserSession(event, {
      user: {
        name: 'John Doe',
      },
    })
    return {}
  }
  throw createError({
    status: 401,
    message: 'Bad credentials',
  })
})
Установите зависимость zod в проект (npm i zod).
Подробнее о серверном хелпере setUserSession из nuxt-auth-utils.

Страница входа

Модуль экспортирует Vue-композабл для проверки, вошёл ли пользователь:

<script setup>
const { loggedIn, session, user, clear, fetch } = useUserSession()
</script>

Сделаем страницу входа с формой, отправляющей данные на /api/login.

app/pages/login.vue
<script setup lang="ts">
const { loggedIn, user, fetch: refreshSession } = useUserSession()
const credentials = reactive({
  email: '',
  password: '',
})
async function login () {
  try {
    await $fetch('/api/login', {
      method: 'POST',
      body: credentials,
    })

    // Refresh the session on client-side and redirect to the home page
    await refreshSession()
    await navigateTo('/')
  } catch {
    alert('Bad credentials')
  }
}
</script>

<template>
  <form @submit.prevent="login">
    <input
      v-model="credentials.email"
      type="email"
      placeholder="Email"
    >
    <input
      v-model="credentials.password"
      type="password"
      placeholder="Password"
    >
    <button type="submit">
      Login
    </button>
  </form>
</template>

Защита API-маршрутов

Клиентский middleware удобен для UX, но без сервера данные всё равно доступны. Критично защищать чувствительные маршруты и отдавать 401, если сессии нет.

В auth-utils есть requireUserSession — проверка активной сессии.

Пример /api/user/stats только для авторизованных:

server/api/user/stats.get.ts
export default defineEventHandler(async (event) => {
  // make sure the user is logged in
  // This will throw a 401 error if the request doesn't come from a valid user session
  const { user } = await requireUserSession(event)

  // TODO: Fetch some stats based on the user

  return {}
})

Защита маршрутов приложения

С серверной защитой данные в порядке, но без клиента неавторизованный пользователь может увидеть странное поведение на /users. Добавим клиентский middleware, чтобы перенаправлять на логин.

nuxt-auth-utils даёт useUserSession — по нему проверяем вход и делаем редирект.

Создаём middleware в каталоге middleware. На клиенте его нужно явно подключать к маршрутам.

app/middleware/authenticated.ts
export default defineNuxtRouteMiddleware(() => {
  const { loggedIn } = useUserSession()

  // redirect the user to the login screen if they're not authenticated
  if (!loggedIn.value) {
    return navigateTo('/login')
  }
})

Главная страница

Подключаем middleware на домашней странице с данными пользователя: без сессии будет редирект на логин.

Используем definePageMeta:

app/pages/index.vue
<script setup lang="ts">
definePageMeta({
  middleware: ['authenticated'],
})

const { user, clear: clearSession } = useUserSession()

async function logout () {
  await clearSession()
  await navigateTo('/login')
}
</script>

<template>
  <div>
    <h1>Welcome {{ user.name }}</h1>
    <button @click="logout">
      Logout
    </button>
  </div>
</template>

Кнопка выхода очищает сессию и ведёт на страницу входа.

Итог

Настроены базовая аутентификация и сессии, защищены чувствительные маршруты на сервере и клиенте.

Дальше можно:

Полный пример с OAuth, БД и CRUD — репозиторий atidone.