- Home »

Создание динамических маршрутов в Next.js с защитой и аутентификацией
Серверная разработка — это не только настройка конфигов nginx и PHP-FPM. Иногда приходится разбираться с полнофункциональными Next.js приложениями, где нужно реализовать динамические маршруты с защитой. Особенно когда ты хостишь фронтенд на собственном сервере, а не в облаке. Эта статья поможет разобраться с файловой системой маршрутизации Next.js, настроить middleware для аутентификации, реализовать защищённые API routes и показать несколько практических кейсов. Разберём как это работает под капотом, пошагово настроим всё с нуля и покажем примеры — от простых до сложных сценариев.
## Как работает система маршрутизации Next.js
Next.js использует файловую систему для создания маршрутов. Каждый файл в папке `pages` (или `app` в новых версиях) автоматически становится маршрутом. Динамические маршруты создаются с помощью квадратных скобок в названии файла:
• `[id].js` — одиночный динамический сегмент
• `[…slug].js` — захватывающие все остальные сегменты
• `[[…slug]].js` — опциональные захватывающие сегменты
Вот пример структуры папок:
pages/
├── api/
│ ├── auth/
│ │ └── [...nextauth].js
│ └── users/
│ └── [id].js
├── dashboard/
│ ├── [category]/
│ │ └── [id].js
│ └── index.js
└── index.js
## Быстрая настройка с нуля: пошаговое руководство
Создаём новый проект Next.js с аутентификацией:
npx create-next-app@latest dynamic-routes-auth
cd dynamic-routes-auth
npm install next-auth
npm install @next-auth/prisma-adapter prisma @prisma/client
npm install bcryptjs jsonwebtoken
### Настройка базы данных
Инициализируем Prisma:
npx prisma init
Создаём схему в `prisma/schema.prisma`:
generator client {
provider = "prisma-client-js"
}
datasource db {
provider = "postgresql"
url = env("DATABASE_URL")
}
model User {
id String @id @default(cuid())
email String @unique
password String
role Role @default(USER)
posts Post[]
}
model Post {
id String @id @default(cuid())
title String
content String
author User @relation(fields: [authorId], references: [id])
authorId String
status Status @default(DRAFT)
}
enum Role {
USER
ADMIN
}
enum Status {
DRAFT
PUBLISHED
}
### Настройка NextAuth.js
Создаём файл `pages/api/auth/[…nextauth].js`:
import NextAuth from 'next-auth'
import CredentialsProvider from 'next-auth/providers/credentials'
import { PrismaAdapter } from "@next-auth/prisma-adapter"
import { prisma } from '../../../lib/prisma'
import bcrypt from 'bcryptjs'
export default NextAuth({
adapter: PrismaAdapter(prisma),
providers: [
CredentialsProvider({
name: 'credentials',
credentials: {
email: { label: 'Email', type: 'email' },
password: { label: 'Password', type: 'password' }
},
async authorize(credentials) {
const user = await prisma.user.findUnique({
where: { email: credentials.email }
})
if (user && bcrypt.compareSync(credentials.password, user.password)) {
return {
id: user.id,
email: user.email,
role: user.role
}
}
return null
}
})
],
callbacks: {
jwt: async ({ token, user }) => {
if (user) {
token.role = user.role
}
return token
},
session: async ({ session, token }) => {
session.user.role = token.role
return session
}
},
pages: {
signIn: '/auth/signin',
error: '/auth/error'
}
})
### Создание middleware для защиты
Создаём файл `middleware.js` в корне проекта:
import { withAuth } from "next-auth/middleware"
export default withAuth(
function middleware(req) {
// Проверяем роль для admin-маршрутов
if (req.nextUrl.pathname.startsWith('/admin') &&
req.nextauth.token?.role !== 'ADMIN') {
return Response.redirect(new URL('/denied', req.url))
}
},
{
callbacks: {
authorized: ({ token, req }) => {
// Защищённые маршруты требуют токен
if (req.nextUrl.pathname.startsWith('/dashboard')) {
return !!token
}
return true
}
}
}
)
export const config = {
matcher: ['/dashboard/:path*', '/admin/:path*', '/api/protected/:path*']
}
## Примеры динамических маршрутов с защитой
### Защищённый API маршрут
Создаём `pages/api/posts/[id].js`:
import { getServerSession } from "next-auth/next"
import { authOptions } from "../auth/[...nextauth]"
import { prisma } from '../../../lib/prisma'
export default async function handler(req, res) {
const session = await getServerSession(req, res, authOptions)
if (!session) {
return res.status(401).json({ error: 'Unauthorized' })
}
const { id } = req.query
switch (req.method) {
case 'GET':
const post = await prisma.post.findUnique({
where: { id },
include: { author: true }
})
if (!post) {
return res.status(404).json({ error: 'Post not found' })
}
return res.json(post)
case 'PUT':
// Только автор или админ могут редактировать
const existingPost = await prisma.post.findUnique({
where: { id }
})
if (!existingPost ||
(existingPost.authorId !== session.user.id &&
session.user.role !== 'ADMIN')) {
return res.status(403).json({ error: 'Forbidden' })
}
const updatedPost = await prisma.post.update({
where: { id },
data: req.body
})
return res.json(updatedPost)
default:
res.setHeader('Allow', ['GET', 'PUT'])
res.status(405).end(`Method ${req.method} Not Allowed`)
}
}
### Динамический страничный маршрут
Создаём `pages/dashboard/posts/[id].js`:
import { useSession } from 'next-auth/react'
import { useRouter } from 'next/router'
import { useState, useEffect } from 'react'
export default function PostPage() {
const { data: session, status } = useSession()
const router = useRouter()
const { id } = router.query
const [post, setPost] = useState(null)
const [loading, setLoading] = useState(true)
useEffect(() => {
if (id) {
fetchPost()
}
}, [id])
const fetchPost = async () => {
try {
const res = await fetch(`/api/posts/${id}`)
if (res.ok) {
const data = await res.json()
setPost(data)
} else {
router.push('/404')
}
} catch (error) {
console.error('Error fetching post:', error)
} finally {
setLoading(false)
}
}
if (status === 'loading' || loading) {
return Loading...
}
if (!session) {
return Access Denied
}
return (
{post?.title}
{post?.content}
Author: {post?.author.email}
)
}
## Сравнение подходов к аутентификации
| Подход | Плюсы | Минусы | Лучше для |
|——–|——-|——–|———–|
| NextAuth.js | Готовые провайдеры, JWT токены | Ограниченная кастомизация | Быстрый старт |
| Custom JWT | Полный контроль | Много boilerplate кода | Сложные случаи |
| Session-based | Простая реализация | Не подходит для SPA | Традиционные MPA |
| OAuth only | Безопасность | Зависимость от провайдеров | Корпоративные приложения |
## Практические кейсы и рекомендации
### Кейс 1: Многоуровневая авторизация
Создаём хук для проверки разрешений:
// hooks/usePermissions.js
import { useSession } from 'next-auth/react'
export function usePermissions() {
const { data: session } = useSession()
const can = (action, resource) => {
if (!session) return false
const permissions = {
ADMIN: ['read', 'write', 'delete'],
USER: ['read', 'write']
}
return permissions[session.user.role]?.includes(action)
}
return { can }
}
### Кейс 2: Защита на уровне компонентов
// components/ProtectedComponent.js
import { useSession } from 'next-auth/react'
import { usePermissions } from '../hooks/usePermissions'
export default function ProtectedComponent({ children, requiredRole }) {
const { data: session } = useSession()
const { can } = usePermissions()
if (!session) {
return Please log in
}
if (requiredRole && !can('read', 'admin')) {
return Access denied
}
return children
}
### Кейс 3: API Rate Limiting
// lib/rateLimit.js
import { LRUCache } from 'lru-cache'
const rateLimit = new LRUCache({
max: 500,
ttl: 60000, // 1 минута
})
export function rateLimiter(identifier) {
const count = rateLimit.get(identifier) || 0
if (count >= 10) {
return false
}
rateLimit.set(identifier, count + 1)
return true
}
## Оптимизация и мониторинг
### Логирование запросов
// lib/logger.js
export function logRequest(req, res, next) {
const start = Date.now()
res.on('finish', () => {
const duration = Date.now() - start
console.log(`${req.method} ${req.url} - ${res.statusCode} - ${duration}ms`)
})
next()
}
### Кэширование для производительности
// lib/cache.js
import NodeCache from 'node-cache'
const cache = new NodeCache({ stdTTL: 600 }) // 10 минут
export function withCache(key, fetchFn) {
return async (...args) => {
const cacheKey = `${key}:${JSON.stringify(args)}`
let result = cache.get(cacheKey)
if (!result) {
result = await fetchFn(...args)
cache.set(cacheKey, result)
}
return result
}
}
## Альтернативные решения
• **Auth0** — внешний сервис аутентификации
• **Firebase Auth** — от Google
• **Supabase Auth** — open-source альтернатива
• **Clerk** — современное решение для React приложений
## Развёртывание на собственном сервере
Для развёртывания на VPS создаём Dockerfile:
FROM node:18-alpine
WORKDIR /app
COPY package*.json ./
RUN npm ci --only=production
COPY . .
RUN npm run build
EXPOSE 3000
CMD ["npm", "start"]
Настройка nginx для проксирования:
server {
listen 80;
server_name your-domain.com;
location / {
proxy_pass http://localhost:3000;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection 'upgrade';
proxy_set_header Host $host;
proxy_cache_bypass $http_upgrade;
}
}
Для высоконагруженных проектов рекомендую использовать выделенный сервер с несколькими инстансами приложения за балансировщиком нагрузки.
## Заключение и рекомендации
Динамические маршруты с защитой в Next.js — это мощный инструмент для создания безопасных веб-приложений. Основные рекомендации:
• Используйте NextAuth.js для быстрого старта, но не бойтесь кастомизировать под свои нужды
• Обязательно валидируйте разрешения как на фронтенде, так и в API
• Применяйте rate limiting для защиты от злоупотреблений
• Логируйте все действия пользователей для аудита безопасности
• Кэшируйте данные для повышения производительности
Этот подход особенно хорош для админ-панелей, пользовательских дашбордов и любых приложений, где нужен гранулярный контроль доступа. При правильной настройке получаете безопасное, масштабируемое решение, которое легко поддерживать и расширять.
В этой статье собрана информация и материалы из различных интернет-источников. Мы признаем и ценим работу всех оригинальных авторов, издателей и веб-сайтов. Несмотря на то, что были приложены все усилия для надлежащего указания исходного материала, любая непреднамеренная оплошность или упущение не являются нарушением авторских прав. Все упомянутые товарные знаки, логотипы и изображения являются собственностью соответствующих владельцев. Если вы считаете, что какой-либо контент, использованный в этой статье, нарушает ваши авторские права, немедленно свяжитесь с нами для рассмотрения и принятия оперативных мер.
Данная статья предназначена исключительно для ознакомительных и образовательных целей и не ущемляет права правообладателей. Если какой-либо материал, защищенный авторским правом, был использован без должного упоминания или с нарушением законов об авторском праве, это непреднамеренно, и мы исправим это незамедлительно после уведомления. Обратите внимание, что переиздание, распространение или воспроизведение части или всего содержимого в любой форме запрещено без письменного разрешения автора и владельца веб-сайта. Для получения разрешений или дополнительных запросов, пожалуйста, свяжитесь с нами.