Cookies и сессии

Зачем нужно

Сессионная аутентификация — классический подход, при котором сервер хранит информацию о пользователе в сессии, а браузер отправляет идентификатор сессии через cookie. В отличие от JWT (stateless), сессии — stateful: состояние хранится на сервере.

Где используется

  • Традиционные веб-приложения (SSR: Rails, Django, PHP)
  • Приложения с высокими требованиями к безопасности (банки)
  • Когда нужно мгновенно разлогинить пользователя (отозвать сессию)
  • Корпоративные приложения

Как работают Cookies

1. Сервер → устанавливает cookie:
   HTTP/1.1 200 OK
   Set-Cookie: session_id=abc123; HttpOnly; Secure; SameSite=Strict

2. Браузер → автоматически отправляет cookie с каждым запросом:
   GET /api/profile
   Cookie: session_id=abc123

3. Сервер → находит сессию по ID, отдаёт данные пользователя
// Установка cookie на сервере (Express)
res.cookie('session_id', 'abc123', {
  httpOnly: true,        // Недоступна для JavaScript (защита от XSS)
  secure: true,          // Только через HTTPS
  sameSite: 'strict',    // Защита от CSRF
  maxAge: 24 * 60 * 60 * 1000,  // 24 часа
  path: '/',             // Для всех путей
  domain: '.example.com', // Для всех поддоменов
});

// Удаление cookie
res.clearCookie('session_id');
Set-Cookie: name=value; HttpOnly; Secure; SameSite=Strict; Max-Age=86400; Path=/; Domain=.example.com
Атрибут Значение Назначение
HttpOnly JS не видит cookie (document.cookie) — защита от XSS
Secure Только через HTTPS
SameSite=Strict Cookie не отправляется при переходе с другого сайта
SameSite=Lax Отправляется при навигации (GET), не при POST
SameSite=None Отправляется всегда (нужен Secure)
Max-Age секунды Время жизни (0 = удалить)
Expires дата Время истечения
Path путь Для каких путей действует
Domain домен Для каких доменов действует

Сессионная аутентификация

const express = require('express');
const session = require('express-session');
const bcrypt = require('bcrypt');

const app = express;
app.use(express.json());

// Настройка сессий
app.use(session({
  secret: process.env.SESSION_SECRET,  // Ключ для подписи cookie
  resave: false,                       // Не пересохранять если не изменилась
  saveUninitialized: false,            // Не создавать пустые сессии
  cookie: {
    httpOnly: true,
    secure: process.env.NODE_ENV === 'production',
    sameSite: 'strict',
    maxAge: 24 * 60 * 60 * 1000,  // 24 часа
  },
  // В production — хранить сессии в Redis/DB, не в памяти
  // store: new RedisStore({ client: redisClient }),
}));

// Логин
app.post('/api/login', async (req, res) => {
  const { email, password } = req.body;

  const user = await db.users.findByEmail(email);
  if (!user) return res.status(401).json({ error: 'Неверные данные' });

  const valid = await bcrypt.compare(password, user.passwordHash);
  if (!valid) return res.status(401).json({ error: 'Неверные данные' });

  // Сохраняем данные в сессию
  req.session.userId = user.id;
  req.session.role = user.role;

  res.json({ message: 'Авторизован', user: { id: user.id, name: user.name } });
});

// Middleware проверки авторизации
function requireAuth(req, res, next) {
  if (!req.session.userId) {
    return res.status(401).json({ error: 'Не авторизован' });
  }
  next;
}

// Защищённый маршрут
app.get('/api/profile', requireAuth, async (req, res) => {
  const user = await db.users.findById(req.session.userId);
  res.json(user);
});

// Логаут
app.post('/api/logout', (req, res) => {
  req.session.destroy((err) => {
    if (err) return res.status(500).json({ error: 'Ошибка' });
    res.clearCookie('connect.sid');
    res.json({ message: 'Вы вышли' });
  });
});

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

// ❌ В памяти (по умолчанию) — теряются при рестарте
app.use(session({ /* ... */ }));

// ✅ Redis (быстро, TTL)
const RedisStore = require('connect-redis').default;
const { createClient } = require('redis');
const redisClient = createClient;

app.use(session({
  store: new RedisStore({ client: redisClient }),
  // ...
}));

// ✅ MongoDB
const MongoStore = require('connect-mongo');

app.use(session({
  store: MongoStore.create({
    mongoUrl: 'mongodb://localhost:27017/sessions',
  }),
  // ...
}));
// Чтение cookies (только НЕ httpOnly)
console.log(document.cookie);
// "theme=dark; lang=ru"

// Установка cookie
document.cookie = 'theme=dark; path=/; max-age=86400';
document.cookie = 'lang=ru; path=/';

// Удаление cookie (установить max-age=0)
document.cookie = 'theme=; max-age=0';

// Вспомогательные функции
function getCookie(name) {
  const match = document.cookie.match(
    new RegExp('(?:^|; )' + name + '=([^;]*)')
  );
  return match ? decodeURIComponent(match[1]) : null;
}

function setCookie(name, value, days = 7) {
  const maxAge = days * 24 * 60 * 60;
  document.cookie = `${name}=${encodeURIComponent(value)}; path=/; max-age=${maxAge}`;
}

function deleteCookie(name) {
  document.cookie = `${name}=; max-age=0; path=/`;
}

Сессии vs JWT

Аспект Сессии JWT
Хранение На сервере На клиенте
Stateful/Stateless Stateful Stateless
Отзыв Мгновенный (удалить сессию) Сложный (blacklist)
Масштабирование Нужен общий store (Redis) Нет состояния на сервере
Размер Cookie ~50 байт (ID) JWT ~500+ байт
Микросервисы Нужен общий store Каждый сервис верифицирует сам
XSS httpOnly защищает В localStorage — уязвим

Частые ошибки

  1. Нет HttpOnly — XSS-атака крадёт session cookie через document.cookie
  2. Нет Secure — cookie передаётся по HTTP, перехват через MITM
  3. SameSite=None без причины — cookie отправляется со всех сайтов (CSRF)
  4. Сессии в памяти — при рестарте сервера все пользователи разлогиниваются
  5. Нет CSRF-защиты — при SameSite=None или Lax нужен CSRF-токен
  6. Огромные cookies — лимит 4KB на cookie, данные хранить в сессии на сервере

Практика

  1. Настроить express-session с cookie-конфигурацией
  2. Реализовать login/logout с сессиями
  3. Добавить requireAuth middleware
  4. Подключить Redis как session store
  5. Проверить в DevTools → Application → Cookies: HttpOnly, Secure, SameSite

Связанные темы

  • JWT — альтернативный stateless подход
  • OAuth 2.0 — OAuth часто завершается созданием сессии
  • HTTP протокол — Set-Cookie заголовок
  • CORS — credentials и кросс-доменные cookies

Ресурсы