Sessions и Cookies в Express

Практическое руководство по настройке session middleware и работе с куками в приложениях на Express.js.

Зачем нужно

Express не имеет встроенной поддержки сессий — она добавляется через express-session. Без понимания настройки хранилища, опций куки и жизненного цикла сессии легко создать небезопасное или нерабочее приложение (например, потеря сессий при перезапуске или утечка через незащищённые куки).

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

  • Веб-приложения с серверной аутентификацией
  • Admin-панели с управлением сессиями
  • MVP и учебные проекты перед внедрением JWT
  • Интеграция с Passport.js (OAuth, Local Strategy)

Минимальная установка

npm install express express-session cookie-parser connect-redis redis

Базовая настройка

const express = require('express');
const session = require('express-session');
const cookieParser = require('cookie-parser');
const RedisStore = require('connect-redis').default;
const { createClient } = require('redis');

const app = express;
app.use(express.json());
app.use(cookieParser(process.env.COOKIE_SECRET));

// Настройка Redis хранилища
const redisClient = createClient({ url: process.env.REDIS_URL });
redisClient.connect.catch(console.error);

// Настройка сессий
app.use(session({
  store: new RedisStore({ client: redisClient }),
  secret: process.env.SESSION_SECRET, // минимум 32 символа
  resave: false,           // не перезаписывать неизменённые сессии
  saveUninitialized: false, // не создавать сессии для анонимов
  name: 'sid',             // имя куки (скрываем 'connect.sid')
  cookie: {
    httpOnly: true,
    secure: process.env.NODE_ENV === 'production',
    sameSite: 'lax',
    maxAge: 2 * 60 * 60 * 1000, // 2 часа
    path: '/',
  },
}));

Аутентификация

const bcrypt = require('bcrypt');

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

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

  // Регенерация session ID — защита от session fixation
  req.session.regenerate((err) => {
    if (err) return res.status(500).end();
    req.session.userId = user.id;
    req.session.role = user.role;
    req.session.loginAt = Date.now();
    res.json({ ok: true, user: { id: user.id, name: user.name } });
  });
});

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

// Middleware — проверка роли
function requireRole(role) {
  return (req, res, next) => {
    if (req.session.role !== role) {
      return res.status(403).json({ error: 'Доступ запрещён' });
    }
    next;
  };
}

// Защищённые маршруты
app.get('/api/me', requireAuth, async (req, res) => {
  const user = await db.users.find(req.session.userId);
  res.json({ id: user.id, name: user.name, email: user.email });
});

app.get('/api/admin/users', requireAuth, requireRole('admin'), async (req, res) => {
  const users = await db.users.findAll;
  res.json(users);
});

// Logout
app.post('/auth/logout', (req, res) => {
  req.session.destroy(err => {
    res.clearCookie('sid', { path: '/' });
    res.json({ ok: true });
  });
});

Работа с куками напрямую

// Установка куки
app.get('/set-prefs', (req, res) => {
  // Простая кука (без подписи)
  res.cookie('lang', 'ru', {
    maxAge: 365 * 24 * 60 * 60 * 1000,
    sameSite: 'lax',
    path: '/',
  });

  // Подписанная кука (защита от подделки, требует cookieParser с secret)
  res.cookie('userId', '42', {
    signed: true,
    httpOnly: true,
    secure: true,
    sameSite: 'lax',
  });

  res.json({ ok: true });
});

// Чтение куки
app.get('/prefs', (req, res) => {
  const lang = req.cookies.lang; // обычная
  const userId = req.signedCookies.userId; // подписанная (false если подделана)
  res.json({ lang, userId });
});

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

  • MemoryStore в продакшне — сессии теряются при рестарте
  • Слишком слабый secret (менее 32 символов) — уязвимость подписи
  • resave: true без необходимости — лишняя запись в Redis на каждый запрос
  • Не вызывают regenerate после логина — session fixation
  • saveUninitialized: true — лишние пустые сессии для анонимных пользователей

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

Ресурсы