Home » Создание динамических маршрутов в Next.js с защитой и аутентификацией
Создание динамических маршрутов в Next.js с защитой и аутентификацией

Создание динамических маршрутов в 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 для защиты от злоупотреблений
• Логируйте все действия пользователей для аудита безопасности
• Кэшируйте данные для повышения производительности

Этот подход особенно хорош для админ-панелей, пользовательских дашбордов и любых приложений, где нужен гранулярный контроль доступа. При правильной настройке получаете безопасное, масштабируемое решение, которое легко поддерживать и расширять.


В этой статье собрана информация и материалы из различных интернет-источников. Мы признаем и ценим работу всех оригинальных авторов, издателей и веб-сайтов. Несмотря на то, что были приложены все усилия для надлежащего указания исходного материала, любая непреднамеренная оплошность или упущение не являются нарушением авторских прав. Все упомянутые товарные знаки, логотипы и изображения являются собственностью соответствующих владельцев. Если вы считаете, что какой-либо контент, использованный в этой статье, нарушает ваши авторские права, немедленно свяжитесь с нами для рассмотрения и принятия оперативных мер.

Данная статья предназначена исключительно для ознакомительных и образовательных целей и не ущемляет права правообладателей. Если какой-либо материал, защищенный авторским правом, был использован без должного упоминания или с нарушением законов об авторском праве, это непреднамеренно, и мы исправим это незамедлительно после уведомления. Обратите внимание, что переиздание, распространение или воспроизведение части или всего содержимого в любой форме запрещено без письменного разрешения автора и владельца веб-сайта. Для получения разрешений или дополнительных запросов, пожалуйста, свяжитесь с нами.

Leave a reply

Your email address will not be published. Required fields are marked