API Routes и проверка сессии

Работа с сессиями в серверных API-роутах строится вокруг идеи безопасного хранения состояния пользователя между запросами без использования внешнего хранилища. Библиотека iron-session реализует этот подход через зашифрованные и подписанные cookie, которые содержат минимально необходимую информацию о сессии.

В основе механизма лежит принцип: сервер не хранит состояние, а доверяет только зашифрованному содержимому cookie, которое невозможно подделать без секретного ключа.

Базовая конфигурация сессии

Настройка начинается с определения параметров шифрования и структуры сессии. Обычно это отдельный модуль, который используется во всех API routes.

// lib/session.js

import { withIronSessionApiRoute } from "iron-session/next";

export const sessionOptions = {
  password: process.env.SESSION_SECRET,
  cookieName: "app_session",
  cookieOptions: {
    secure: process.env.NODE_ENV === "production",
  },
};

Ключевой параметр password должен быть достаточно длинным (не менее 32 символов), поскольку он используется для шифрования данных сессии.

Подключение сессии к API route

API route в Next.js оборачивается функцией-обёрткой, которая добавляет объект req.session.

// pages/api/login.js

import { withIronSessionApiRoute } from "iron-session/next";
import { sessionOptions } from "../. ./lib/session";

export default withIronSessionApiRoute(loginRoute, sessionOptions);

async function loginRoute(req, res) {
  const { username, password } = req.body;

  if (username === "admin" && password === "1234") {
    req.session.user = {
      id: 1,
      username: "admin",
      role: "admin",
    };

    await req.session.save();
    return res.status(200).json({ ok: true });
  }

  res.status(401).json({ error: "Invalid credentials" });
}

После успешной аутентификации объект пользователя записывается в req.session.user, а затем сохраняется в зашифрованной cookie.

Получение данных сессии в API routes

Каждый защищённый маршрут может получать доступ к данным сессии напрямую через req.session.

// pages/api/profile.js

import { withIronSessionApiRoute } from "iron-session/next";
import { sessionOptions } from "../. ./lib/session";

export default withIronSessionApiRoute(profileRoute, sessionOptions);

async function profileRoute(req, res) {
  const user = req.session.user;

  if (!user) {
    return res.status(401).json({ error: "Unauthorized" });
  }

  res.json({
    id: user.id,
    username: user.username,
    role: user.role,
  });
}

Проверка авторизации сводится к проверке наличия объекта req.session.user.

Очистка сессии (logout)

Удаление сессии выполняется через явное уничтожение данных.

// pages/api/logout.js

import { withIronSessionApiRoute } from "iron-session/next";
import { sessionOptions } from "../. ./lib/session";

export default withIronSessionApiRoute(logoutRoute, sessionOptions);

async function logoutRoute(req, res) {
  req.session.destroy();
  res.json({ ok: true });
}

После вызова destroy() cookie удаляется на стороне клиента, а данные становятся недействительными.

Проверка сессии на уровне middleware-подобной логики

В API routes часто требуется единый механизм защиты. Это можно реализовать через функцию-обёртку.

export function requireAuth(handler) {
  return withIronSessionApiRoute(async (req, res) => {
    if (!req.session.user) {
      return res.status(401).json({ error: "Unauthorized" });
    }

    return handler(req, res);
  }, sessionOptions);
}

Использование:

// pages/api/admin.js

async function adminRoute(req, res) {
  if (req.session.user.role !== "admin") {
    return res.status(403).json({ error: "Forbidden" });
  }

  res.json({ secret: "admin data" });
}

export default requireAuth(adminRoute);

Такой подход позволяет централизовать проверку авторизации и уменьшить дублирование кода.

Обновление данных сессии

Сессия может изменяться в процессе работы приложения, например при обновлении профиля пользователя.

req.session.user.username = "newName";
await req.session.save();

Важно явно вызывать save(), иначе изменения не будут записаны в cookie.

Структура данных сессии

Сессия представляет собой обычный JavaScript-объект, но хранить в ней следует только минимально необходимую информацию:

  • идентификатор пользователя
  • роль или уровень доступа
  • базовые публичные данные профиля

Не рекомендуется хранить:

  • пароли
  • токены доступа к внешним сервисам без необходимости
  • большие объёмы данных

Причина заключается в ограничении размера cookie и необходимости минимизации поверхности атаки.

Поведение на клиенте через API

Клиентская часть взаимодействует с API routes стандартными запросами:

const res = await fetch("/api/profile");
const data = await res.json();

Если сессия отсутствует, сервер возвращает 401, и клиент может перенаправить пользователя на страницу входа.

Проверка сессии при каждом запросе

Так как cookie отправляется автоматически, каждый запрос к API может использоваться как источник проверки авторизации. Это делает архитектуру stateless с точки зрения сервера, при этом сохраняется возможность хранения состояния пользователя.

Безопасность и шифрование

iron-session использует:

  • шифрование содержимого cookie
  • подпись для предотвращения подмены данных
  • серверный секрет, который никогда не отправляется клиенту

Даже если пользователь получит cookie, её содержимое невозможно изменить без обнаружения подделки.

Типичная структура проекта с API routes

/lib
  session.js
/pages
  /api
    login.js
    logout.js
    profile.js
    admin.js

Централизация конфигурации сессии позволяет использовать единые правила безопасности во всех маршрутах.

Расширение модели сессии

По мере роста приложения структура сессии может расширяться:

req.session.user = {
  id: 1,
  username: "admin",
  role: "admin",
  permissions: ["read", "write"],
  lastLogin: Date.now(),
};

При этом важно сохранять баланс между удобством и размером cookie, так как чрезмерное увеличение данных приводит к проблемам с заголовками HTTP.

Разделение публичных и защищённых API routes

Часто API разделяются на:

  • публичные (не требуют сессии)
  • защищённые (требуют проверки req.session.user)

Это разделение обычно реализуется через отдельные директории или обёртки.

if (!req.session.user) {
  return res.status(401).end();
}

Такой подход формирует базовый слой защиты, поверх которого строится бизнес-логика маршрутов.