Перейти к содержимому

17. Аутентификация

Аутентификация — одна из ключевых задач любого веб-приложения. Nuxt 3 предлагает несколько подходов: от готовых модулей до реализации с нуля через composables и server routes.


Официальный минималистичный модуль от команды Nuxt для управления сессиями:

Окно терминала
npx nuxi module add auth-utils
nuxt.config.ts
export default defineNuxtConfig({
modules: ['nuxt-auth-utils']
})
Окно терминала
NUXT_SESSION_PASSWORD=at-least-32-chars-long-secret-key!!
server/routes/auth/github.get.ts
export default defineOAuthGitHubEventHandler({
config: {
clientId: process.env.GITHUB_CLIENT_ID,
clientSecret: process.env.GITHUB_CLIENT_SECRET
},
async onSuccess(event, { user, tokens }) {
await setUserSession(event, {
user: {
id: user.id,
name: user.name,
email: user.email,
avatar: user.avatar_url,
provider: 'github'
}
})
return sendRedirect(event, '/dashboard')
},
onError(event, error) {
console.error('GitHub OAuth error:', error)
return sendRedirect(event, '/login?error=github')
}
})
server/routes/auth/google.get.ts
export default defineOAuthGoogleEventHandler({
config: {
clientId: process.env.GOOGLE_CLIENT_ID,
clientSecret: process.env.GOOGLE_CLIENT_SECRET,
scope: ['openid', 'email', 'profile']
},
async onSuccess(event, { user }) {
await setUserSession(event, {
user: {
id: user.sub,
name: user.name,
email: user.email,
avatar: user.picture,
provider: 'google'
}
})
return sendRedirect(event, '/dashboard')
}
})

<script setup lang="ts">
const { loggedIn, user, session, fetch, clear } = useUserSession()
// loggedIn — boolean, авторизован ли пользователь
// user — данные пользователя
// session — полная сессия
// fetch — перезагрузить сессию
// clear — выйти
</script>
<template>
<div v-if="loggedIn">
<img :src="user.avatar" />
<p>Привет, {{ user.name }}!</p>
<button @click="clear">Выйти</button>
</div>
<div v-else>
<NuxtLink to="/login">Войти</NuxtLink>
</div>
</template>

Мощный модуль с поддержкой NextAuth.js / Auth.js:

Окно терминала
npx nuxi module add @sidebase/nuxt-auth
nuxt.config.ts
export default defineNuxtConfig({
modules: ['@sidebase/nuxt-auth'],
auth: {
provider: {
type: 'authjs'
},
globalAppMiddleware: {
isEnabled: true
}
}
})
server/api/auth/[...].ts
import { NuxtAuthHandler } from '#auth'
import GithubProvider from 'next-auth/providers/github'
import GoogleProvider from 'next-auth/providers/google'
import CredentialsProvider from 'next-auth/providers/credentials'
import { PrismaAdapter } from '@auth/prisma-adapter'
import { prisma } from '~/server/utils/prisma'
export default NuxtAuthHandler({
adapter: PrismaAdapter(prisma),
secret: process.env.NEXTAUTH_SECRET,
providers: [
GithubProvider({
clientId: process.env.GITHUB_CLIENT_ID,
clientSecret: process.env.GITHUB_CLIENT_SECRET
}),
GoogleProvider({
clientId: process.env.GOOGLE_CLIENT_ID,
clientSecret: process.env.GOOGLE_CLIENT_SECRET
}),
CredentialsProvider({
name: 'credentials',
credentials: {
email: { label: 'Email', type: 'email' },
password: { label: 'Пароль', type: 'password' }
},
async authorize(credentials) {
const user = await prisma.user.findUnique({
where: { email: credentials.email }
})
if (user && await bcrypt.compare(credentials.password, user.password)) {
return { id: user.id, email: user.email, name: user.name }
}
return null
}
})
],
callbacks: {
jwt({ token, user }) {
if (user) {
token.id = user.id
token.role = user.role
}
return token
},
session({ session, token }) {
session.user.id = token.id as string
session.user.role = token.role as string
return session
}
},
pages: {
signIn: '/auth/login',
error: '/auth/error'
}
})

<script setup lang="ts">
const { status, data, signIn, signOut, getCsrfToken } = useAuth()
// status: 'authenticated' | 'unauthenticated' | 'loading'
// data: сессия с данными пользователя
const handleSignIn = () => {
signIn('github', { callbackUrl: '/dashboard' })
}
const handleSignOut = () => {
signOut({ callbackUrl: '/' })
}
</script>
<template>
<div>
<div v-if="status === 'loading'">Загрузка...</div>
<div v-else-if="status === 'authenticated'">
<p>{{ data.user.name }}</p>
<button @click="handleSignOut">Выйти</button>
</div>
<div v-else>
<button @click="handleSignIn">Войти через GitHub</button>
</div>
</div>
</template>

middleware/auth.ts
export default defineNuxtRouteMiddleware((to) => {
const { status } = useAuth()
if (status.value === 'unauthenticated') {
return navigateTo({
path: '/auth/login',
query: { redirect: to.fullPath }
})
}
})
middleware/admin.ts
export default defineNuxtRouteMiddleware((to) => {
const { data } = useAuth()
if (data.value?.user?.role !== 'admin') {
return navigateTo('/403')
}
})
pages/admin/index.vue
<script setup>
definePageMeta({
middleware: ['auth', 'admin']
})
</script>

Реализация JWT без внешних модулей:

server/utils/jwt.ts
import jwt from 'jsonwebtoken'
const SECRET = process.env.JWT_SECRET!
export function signToken(payload: object) {
return jwt.sign(payload, SECRET, { expiresIn: '15m' })
}
export function signRefreshToken(payload: object) {
return jwt.sign(payload, SECRET + '_refresh', { expiresIn: '7d' })
}
export function verifyToken(token: string) {
return jwt.verify(token, SECRET) as jwt.JwtPayload
}
export function verifyRefreshToken(token: string) {
return jwt.verify(token, SECRET + '_refresh') as jwt.JwtPayload
}
server/api/auth/login.post.ts
export default defineEventHandler(async (event) => {
const { email, password } = await readBody(event)
const user = await prisma.user.findUnique({ where: { email } })
if (!user || !await bcrypt.compare(password, user.password)) {
throw createError({ statusCode: 401, message: 'Неверные учётные данные' })
}
const accessToken = signToken({ id: user.id, email: user.email, role: user.role })
const refreshToken = signRefreshToken({ id: user.id })
// Сохраняем refresh token в httpOnly cookie
setCookie(event, 'refresh_token', refreshToken, {
httpOnly: true,
secure: process.env.NODE_ENV === 'production',
sameSite: 'strict',
maxAge: 60 * 60 * 24 * 7 // 7 дней
})
return { accessToken, user: { id: user.id, name: user.name, email: user.email } }
})

server/api/auth/refresh.post.ts
export default defineEventHandler(async (event) => {
const refreshToken = getCookie(event, 'refresh_token')
if (!refreshToken) {
throw createError({ statusCode: 401, message: 'Refresh token отсутствует' })
}
try {
const payload = verifyRefreshToken(refreshToken)
const user = await prisma.user.findUnique({ where: { id: payload.id } })
if (!user) {
throw createError({ statusCode: 401, message: 'Пользователь не найден' })
}
const newAccessToken = signToken({
id: user.id,
email: user.email,
role: user.role
})
return { accessToken: newAccessToken }
} catch {
deleteCookie(event, 'refresh_token')
throw createError({ statusCode: 401, message: 'Недействительный refresh token' })
}
})

ПодходСложностьГибкостьКогда использовать
nuxt-auth-utils⭐ Низкая🔧 СредняяOAuth, быстрый старт
@sidebase/nuxt-auth⭐⭐ Средняя🔧🔧 ВысокаяКомплексные проекты
JWT вручную⭐⭐⭐ Высокая🔧🔧🔧 ПолнаяКастомная логика
Lucia Auth⭐⭐ Средняя🔧🔧 ВысокаяТипобезопасность