Session-based аутентификация

Session-based аутентификация строится вокруг идеи серверной сессии, связанной с конкретным клиентом. После успешной аутентификации сервер создаёт сессию, сохраняет её состояние и передаёт клиенту идентификатор сессии, как правило, через cookie. При каждом последующем запросе клиент автоматически отправляет этот идентификатор, а сервер использует его для восстановления контекста пользователя.

Fresh, как серверный фреймворк для Deno, изначально ориентирован на HTTP-first архитектуру и тесную работу с Web API. Это делает реализацию сессионной аутентификации максимально прозрачной и близкой к стандартам платформы.


Роль cookies в Fresh

В Fresh нет встроенного механизма сессий, как в классических MVC-фреймворках. Работа с cookies осуществляется напрямую через HTTP-заголовки.

Основные особенности:

  • cookies устанавливаются через заголовок Set-Cookie
  • чтение cookies происходит из заголовка Cookie
  • управление временем жизни и безопасностью полностью лежит на сервере

Пример установки cookie после логина:

return new Response(null, {
  status: 302,
  headers: {
    "Location": "/",
    "Set-Cookie": "session_id=abc123; HttpOnly; Path=/; SameSite=Lax"
  }
});

Ключевые параметры:

  • HttpOnly — защита от доступа через JavaScript
  • SameSite — защита от CSRF
  • Secure — обязательный флаг при использовании HTTPS
  • Path — область действия cookie

Хранение сессий на сервере

Сессионные данные никогда не хранятся в cookie целиком. Cookie содержит только идентификатор, а реальные данные располагаются на сервере.

В Fresh чаще всего используются:

  • in-memory хранилища (Map)
  • Redis
  • KV-хранилища Deno
  • базы данных

Простейшая модель:

const sessions = new Map<string, { userId: string }>();

После аутентификации:

const sessionId = crypto.randomUUID();
sessions.set(sessionId, { userId });

Такой подход подходит только для разработки или single-instance окружений.


Middleware для восстановления сессии

Fresh использует middleware на уровне routes/_middleware.ts. Это ключевая точка для восстановления пользователя из сессии.

Пример базового middleware:

import { MiddlewareHandlerContext } from "$fresh/server.ts";

export async function handler(
  req: Request,
  ctx: MiddlewareHandlerContext
) {
  const cookie = req.headers.get("cookie");
  const sessionId = cookie?.match(/session_id=([^;]+)/)?.[1];

  if (sessionId) {
    const session = sessions.get(sessionId);
    if (session) {
      ctx.state.user = session.userId;
    }
  }

  return await ctx.next();
}

Важные моменты:

  • middleware выполняется до рендеринга страницы
  • ctx.state используется для передачи данных дальше по цепочке
  • отсутствие сессии не считается ошибкой

Защищённые маршруты

Проверка аутентификации выполняется либо в middleware, либо непосредственно в обработчике маршрута.

Пример защиты страницы:

export const handler = {
  GET(req, ctx) {
    if (!ctx.state.user) {
      return new Response(null, {
        status: 302,
        headers: { Location: "/login" }
      });
    }

    return ctx.render();
  }
};

Подход с редиректом предпочтителен для UI-маршрутов, тогда как API-маршруты обычно возвращают 401 Unauthorized.


Интеграция с island-компонентами

Fresh использует islands architecture, при которой большая часть логики остаётся на сервере. Это хорошо сочетается с session-based аутентификацией.

Данные пользователя передаются в JSX:

export default function Page(props) {
  return (
    <div>
      <p>Пользователь: {props.user}</p>
    </div>
  );
}

И прокидываются через ctx.render:

return ctx.render({ user: ctx.state.user });

JavaScript на клиенте не имеет прямого доступа к cookie с флагом HttpOnly, что повышает безопасность.


Logout и уничтожение сессии

Выход из системы заключается в двух шагах:

  1. удаление сессии на сервере
  2. удаление cookie у клиента

Пример обработчика:

sessions.delete(sessionId);

return new Response(null, {
  status: 302,
  headers: {
    "Location": "/login",
    "Set-Cookie": "session_id=; Max-Age=0; Path=/"
  }
});

Cookie с Max-Age=0 считается немедленно истёкшим.


CSRF и session-based аутентификация

Session-based подход уязвим к CSRF-атакам, если не применяются защитные меры.

Обязательные элементы защиты:

  • SameSite=Lax или SameSite=Strict
  • CSRF-токены для форм и mutating-запросов
  • проверка Origin и Referer для API

Пример генерации CSRF-токена в сессии:

session.csrf = crypto.randomUUID();

И проверки в POST-запросе:

if (formData.get("csrf") !== session.csrf) {
  return new Response("Forbidden", { status: 403 });
}

Масштабирование и ограничения

Session-based аутентификация в Fresh имеет следующие особенности:

  • требуется централизованное хранилище сессий при масштабировании
  • сервер хранит состояние, что усложняет горизонтальное масштабирование
  • высокая безопасность при правильной настройке cookies

Для production-сценариев предпочтительно:

  • Redis или Deno KV
  • ротация session_id
  • ограничение времени жизни сессий
  • логирование подозрительной активности

Сравнение с token-based подходами

Session-based аутентификация в Fresh:

  • идеально подходит для SSR
  • минимизирует логику на клиенте
  • хорошо интегрируется с islands architecture
  • опирается на стандартные HTTP-механизмы

В контексте Fresh этот подход считается базовым и наиболее естественным, так как фреймворк изначально проектировался вокруг серверного рендеринга и минимального клиентского состояния.