3. Сессии и куки
Что такое HTTP-сессия?
Заголовок раздела «Что такое HTTP-сессия?»HTTP — протокол без состояния (stateless). Каждый запрос независим. Сессии решают проблему “запомнить пользователя” между запросами.
Механизм сессий:
- Пользователь вводит логин/пароль
- Сервер создаёт сессию и сохраняет её (в памяти, Redis, БД)
- Сервер отправляет клиенту cookie с Session ID
- При каждом запросе браузер отправляет cookie
- Сервер находит сессию по Session ID
Client Server │ │ │─── POST /login ─────────▶│ │ │─── db.sessions.create({userId, data}) │ │ │◀── Set-Cookie: sid=abc ──│ │ │ │─── GET /profile ─────────│ │ Cookie: sid=abc │ │ │─── db.sessions.find({id: 'abc'}) │◀── 200 OK (user data) ───│Куки (Cookies)
Заголовок раздела «Куки (Cookies)»Куки — небольшие фрагменты данных, хранимые браузером.
Создание куки
Заголовок раздела «Создание куки»// Простая кукаres.cookie('name', 'value');
// Кука с настройками безопасностиres.cookie('sessionId', sessionId, { httpOnly: true, // недоступна JavaScript — защита от XSS secure: true, // только HTTPS sameSite: 'Strict', // защита от CSRF maxAge: 7 * 24 * 60 * 60 * 1000, // 7 дней в мс path: '/',});Флаги безопасности куки
Заголовок раздела «Флаги безопасности куки»| Флаг | Описание | Зачем |
|---|---|---|
HttpOnly | Недоступна JavaScript | Защита от XSS |
Secure | Только HTTPS | Защита от MITM |
SameSite=Strict | Не отправляется с других сайтов | Защита от CSRF |
SameSite=Lax | Отправляется при навигации (default) | Баланс |
SameSite=None | Отправляется всегда (нужен Secure) | Для iframe |
Path | Ограничивает путь | Минимизация данных |
Domain | Поддомены | Шаринг сессий |
Expires/MaxAge | Время жизни | Авто-удаление |
express-session
Заголовок раздела «express-session»Стандартный модуль для сессий в Express:
npm install express-sessionБазовая настройка
Заголовок раздела «Базовая настройка»import express from 'express';import session from 'express-session';
const app = express();
app.use(session({ secret: process.env.SESSION_SECRET, // длинная случайная строка resave: false, // не сохранять если не изменилась saveUninitialized: false, // не создавать пустые сессии cookie: { httpOnly: true, secure: process.env.NODE_ENV === 'production', sameSite: 'Lax', maxAge: 7 * 24 * 60 * 60 * 1000, // 7 дней }}));
// Теперь req.session доступнаapp.post('/login', async (req, res) => { const { email, password } = req.body; const user = await authenticateUser(email, password);
// Сохраняем данные в сессии req.session.userId = user.id; req.session.email = user.email;
res.json({ success: true });});
app.get('/profile', (req, res) => { if (!req.session.userId) { return res.status(401).json({ error: 'Not authenticated' }); }
res.json({ userId: req.session.userId });});
app.post('/logout', (req, res) => { req.session.destroy((err) => { res.clearCookie('connect.sid'); res.json({ success: true }); });});SESSION_SECRET
Заголовок раздела «SESSION_SECRET»Secret должен быть длинным и случайным:
# Генерация в терминалеnode -e "console.log(require('crypto').randomBytes(64).toString('hex'))"SESSION_SECRET=a1b2c3d4...очень-длинная-строка...z9y8x7Хранилище сессий
Заголовок раздела «Хранилище сессий»По умолчанию express-session хранит сессии в памяти. Это плохо для production!
Redis (рекомендуется)
Заголовок раздела «Redis (рекомендуется)»npm install connect-redis redisimport { createClient } from 'redis';import { RedisStore } from 'connect-redis';
const redisClient = createClient({ url: process.env.REDIS_URL || 'redis://localhost:6379'});
await redisClient.connect();
app.use(session({ store: new RedisStore({ client: redisClient }), secret: process.env.SESSION_SECRET, resave: false, saveUninitialized: false, cookie: { httpOnly: true, secure: true, sameSite: 'Lax', maxAge: 7 * 24 * 60 * 60 * 1000, }}));PostgreSQL
Заголовок раздела «PostgreSQL»npm install connect-pg-simpleimport pgSession from 'connect-pg-simple';
const PgSession = pgSession(session);
app.use(session({ store: new PgSession({ conString: process.env.DATABASE_URL, tableName: 'sessions', createTableIfMissing: true, }), // ...остальные настройки}));Middleware для защиты роутов
Заголовок раздела «Middleware для защиты роутов»// Middleware проверки аутентификацииfunction requireAuth(req, res, next) { if (!req.session.userId) { return res.status(401).json({ error: 'Authentication required' }); } next();}
// Загрузка пользователя в каждый запросasync function loadUser(req, res, next) { if (req.session.userId) { req.user = await db.users.findById(req.session.userId); } next();}
app.use(loadUser);
// Защищённый роутapp.get('/dashboard', requireAuth, (req, res) => { res.json({ user: req.user });});Безопасность сессий
Заголовок раздела «Безопасность сессий»Session Fixation Attack
Заголовок раздела «Session Fixation Attack»Злоумышленник устанавливает Session ID жертве заранее, затем ждёт входа.
Защита: регенерировать Session ID после логина!
app.post('/login', async (req, res) => { const user = await authenticateUser(req.body.email, req.body.password);
// Регенерируем Session ID перед сохранением данных req.session.regenerate((err) => { if (err) return res.status(500).json({ error: 'Server error' });
req.session.userId = user.id; req.session.save((err) => { if (err) return res.status(500).json({ error: 'Server error' }); res.json({ success: true }); }); });});Ограничение срока жизни
Заголовок раздела «Ограничение срока жизни»// Проверяем последнюю активностьfunction checkSessionExpiry(req, res, next) { const IDLE_TIMEOUT = 30 * 60 * 1000; // 30 минут
if (req.session.lastActivity) { const idle = Date.now() - req.session.lastActivity;
if (idle > IDLE_TIMEOUT) { req.session.destroy(); return res.status(401).json({ error: 'Session expired' }); } }
req.session.lastActivity = Date.now(); next();}Привязка к IP (опционально)
Заголовок раздела «Привязка к IP (опционально)»function bindSessionToIP(req, res, next) { if (req.session.ip && req.session.ip !== req.ip) { // IP изменился — возможна кража сессии req.session.destroy(); return res.status(401).json({ error: 'Session invalid' }); }
if (!req.session.ip) { req.session.ip = req.ip; }
next();}Практические задания
Заголовок раздела «Практические задания»- Настрой express-session с Redis хранилищем
- Реализуй login/logout с регенерацией session ID
- Добавь middleware для проверки аутентификации
- Реализуй “запомнить меня” (Remember Me) с длинным сроком сессии