9. Server Middleware
🛡️ Server Middleware в Nuxt 3
Заголовок раздела «🛡️ Server Middleware в Nuxt 3»Server Middleware в Nuxt 3 выполняется перед каждым запросом к серверу. Это идеальное место для логирования, CORS, аутентификации, rate limiting и любой сквозной серверной логики.
Что такое Server Middleware? 🔮
Заголовок раздела «Что такое Server Middleware? 🔮»HTTP запрос ↓[Server Middleware 1] — logger.ts ↓[Server Middleware 2] — cors.ts ↓[Server Middleware 3] — auth.ts ↓Обработчик маршрута (api/users.get.ts) ↓HTTP ответServer Middleware — это функция defineEventHandler, которая:
- Получает объект события
H3Event - Может модифицировать запрос/ответ
- Может прервать цепочку (выбросить ошибку)
- Может ничего не возвращать (передать дальше)
Создание первого Middleware 📝
Заголовок раздела «Создание первого Middleware 📝»export default defineEventHandler((event) => { const start = Date.now() const method = getMethod(event) const url = getRequestURL(event)
// Логируем после завершения запроса event.node.res.on('finish', () => { const duration = Date.now() - start const status = event.node.res.statusCode
console.log( \`[\${new Date().toISOString()}] \${method} \${url.pathname} → \${status} (\${duration}ms)\` ) })
// Не возвращаем ничего — передаём дальше})Порядок выполнения Middleware 🔢
Заголовок раздела «Порядок выполнения Middleware 🔢»Middleware выполняются в алфавитном порядке имён файлов:
server/middleware/├── 01.logger.ts ← Выполняется первым├── 02.cors.ts ← Вторым├── 03.auth.ts ← Третьим└── 04.rate-limit.ts ← ПоследнимCORS Middleware 🌐
Заголовок раздела «CORS Middleware 🌐»export default defineEventHandler((event) => { const allowedOrigins = [ 'https://myapp.com', 'https://admin.myapp.com', process.env.NODE_ENV === 'development' ? 'http://localhost:3000' : '' ].filter(Boolean)
const origin = getHeader(event, 'origin')
// Проверяем origin if (origin && allowedOrigins.includes(origin)) { setHeader(event, 'Access-Control-Allow-Origin', origin) }
setHeader(event, 'Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, PATCH, OPTIONS') setHeader(event, 'Access-Control-Allow-Headers', 'Content-Type, Authorization, X-Requested-With') setHeader(event, 'Access-Control-Allow-Credentials', 'true') setHeader(event, 'Access-Control-Max-Age', '86400') // 24 часа preflight кэш
// Обрабатываем OPTIONS preflight запросы if (getMethod(event) === 'OPTIONS') { setResponseStatus(event, 204) return '' // Завершаем запрос }})Auth Middleware 🔐
Заголовок раздела «Auth Middleware 🔐»export default defineEventHandler(async (event) => { // Маршруты, не требующие аутентификации const publicPaths = [ '/api/auth/login', '/api/auth/register', '/api/auth/refresh', '/api/public', ]
const path = getRequestURL(event).pathname
// Пропускаем публичные маршруты if (publicPaths.some(p => path.startsWith(p))) return
// Проверяем только API маршруты if (!path.startsWith('/api/')) return
// Ищем токен в заголовке или cookie const authHeader = getHeader(event, 'authorization') const token = authHeader?.startsWith('Bearer ') ? authHeader.slice(7) : getCookie(event, 'auth-token')
if (!token) { throw createError({ statusCode: 401, statusMessage: 'Токен не предоставлен', }) }
try { // Верифицируем JWT const payload = verifyJWT(token) // Добавляем пользователя в контекст event.context.user = payload } catch { throw createError({ statusCode: 401, statusMessage: 'Недействительный токен', }) }})Rate Limiting Middleware ⚡
Заголовок раздела «Rate Limiting Middleware ⚡»// Простое in-memory хранилище (используй Redis в продакшне)const requestCounts = new Map<string, { count: number; resetAt: number }>()
const RATE_LIMIT = 100 // запросовconst WINDOW_MS = 60 * 1000 // за минуту
export default defineEventHandler((event) => { const ip = event.node.req.socket.remoteAddress || 'unknown' const now = Date.now()
let record = requestCounts.get(ip)
if (!record || now > record.resetAt) { record = { count: 0, resetAt: now + WINDOW_MS } requestCounts.set(ip, record) }
record.count++
// Добавляем заголовки setHeader(event, 'X-RateLimit-Limit', String(RATE_LIMIT)) setHeader(event, 'X-RateLimit-Remaining', String(Math.max(0, RATE_LIMIT - record.count))) setHeader(event, 'X-RateLimit-Reset', String(record.resetAt))
if (record.count > RATE_LIMIT) { throw createError({ statusCode: 429, statusMessage: 'Too Many Requests', data: { retryAfter: Math.ceil((record.resetAt - now) / 1000) } }) }})Request Modification Middleware 🔧
Заголовок раздела «Request Modification Middleware 🔧»import { randomUUID } from 'crypto'
export default defineEventHandler((event) => { // Генерируем уникальный ID запроса const requestId = getHeader(event, 'x-request-id') || randomUUID()
// Сохраняем в контексте event.context.requestId = requestId
// Добавляем в ответ setHeader(event, 'X-Request-ID', requestId)})export default defineEventHandler(async (event) => { // Только для запросов с телом const method = getMethod(event) if (!['POST', 'PUT', 'PATCH'].includes(method)) return
const contentType = getHeader(event, 'content-type') || ''
// Валидируем Content-Type if (contentType.includes('application/json')) { try { event.context.parsedBody = await readBody(event) } catch { throw createError({ statusCode: 400, statusMessage: 'Неверный формат JSON', }) } }})Сессии с H3 💾
Заголовок раздела «Сессии с H3 💾»export default defineEventHandler(async (event) => { // Используем nuxt-auth-utils или свою реализацию const sessionId = getCookie(event, 'session_id')
if (sessionId) { // Загружаем сессию из хранилища const session = await storage.getItem(\`sessions:\${sessionId}\`) if (session) { event.context.session = session } }})export default defineEventHandler(async (event) => { const session = event.context.session || {}
if (!session.visits) { session.visits = 0 } session.visits++ session.lastVisit = new Date().toISOString()
// Сохраняем обновлённую сессию let sessionId = getCookie(event, 'session_id') if (!sessionId) { sessionId = crypto.randomUUID() setCookie(event, 'session_id', sessionId, { httpOnly: true, maxAge: 60 * 60 * 24 * 7 }) }
await storage.setItem(\`sessions:\${sessionId}\`, session)
return { visits: session.visits, lastVisit: session.lastVisit }})Middleware для конкретных маршрутов 🎯
Заголовок раздела «Middleware для конкретных маршрутов 🎯»// server/api/admin/[...].ts — middleware только для /api/admin/**export default defineEventHandler(async (event) => { const user = event.context.user
if (!user) { throw createError({ statusCode: 401, statusMessage: 'Unauthorized' }) }
if (user.role !== 'admin') { throw createError({ statusCode: 403, statusMessage: 'Forbidden' }) }
// Передаём в следующий обработчик})Глобальный обработчик ошибок 🚨
Заголовок раздела «Глобальный обработчик ошибок 🚨»export default defineNitroPlugin((nitroApp) => { nitroApp.hooks.hook('error', (error, { event }) => { // Логируем все серверные ошибки console.error('Server Error:', { url: event?.path, statusCode: error.statusCode, message: error.message, stack: error.stack, })
// Оповещаем Sentry или другой сервис // sentry.captureException(error) })})Security Headers Middleware 🔒
Заголовок раздела «Security Headers Middleware 🔒»export default defineEventHandler((event) => { // Защита от XSS setHeader(event, 'X-Content-Type-Options', 'nosniff') setHeader(event, 'X-Frame-Options', 'SAMEORIGIN') setHeader(event, 'X-XSS-Protection', '1; mode=block')
// HSTS (только HTTPS) if (event.node.req.connection?.encrypted) { setHeader(event, 'Strict-Transport-Security', 'max-age=31536000; includeSubDomains') }
// Content Security Policy setHeader(event, 'Content-Security-Policy', [ "default-src 'self'", "script-src 'self' 'unsafe-inline'", "style-src 'self' 'unsafe-inline'", "img-src 'self' data: https:", "font-src 'self'", "connect-src 'self'", ].join('; '))
// Referrer Policy setHeader(event, 'Referrer-Policy', 'strict-origin-when-cross-origin')
// Permissions Policy setHeader(event, 'Permissions-Policy', 'camera=(), microphone=(), geolocation=()')})