18. Sessions и Cookies

Сессии и куки — альтернатива JWT. Удобны для веб-приложений, где сервер хранит состояние пользователя.
Cookie — основы
Заголовок раздела «Cookie — основы»npm install cookie-parserconst cookieParser = require('cookie-parser');
// Подключаем (нужен для чтения кук)app.use(cookieParser(process.env.COOKIE_SECRET)); // секрет для подписанных кук
// Установка кукиapp.get('/set-cookie', (req, res) => { // Простая кука res.cookie('language', 'ru');
// С опциями res.cookie('theme', 'dark', { maxAge: 30 * 24 * 60 * 60 * 1000, // 30 дней в мс httpOnly: false, // доступна из JS (нужна для темы) secure: process.env.NODE_ENV === 'production', sameSite: 'lax', });
// Подписанная кука (защита от подделки) res.cookie('userId', '42', { signed: true, // подписывается с cookie.secret httpOnly: true, secure: true, maxAge: 7 * 24 * 60 * 60 * 1000, });
res.json({ message: 'Куки установлены!' });});
// Чтение кукиapp.get('/read-cookie', (req, res) => { console.log(req.cookies); // { language: 'ru', theme: 'dark' } console.log(req.signedCookies); // { userId: '42' }
const theme = req.cookies.theme; const userId = req.signedCookies.userId; // автоматически проверяет подпись
res.json({ theme, userId });});
// Удаление кукиapp.get('/clear', (req, res) => { res.clearCookie('theme'); res.clearCookie('userId'); res.json({ message: 'Куки удалены' });});Настройки безопасности куки
Заголовок раздела «Настройки безопасности куки»// Лучшие практики для auth кукиres.cookie('session', sessionId, { httpOnly: true, // ❗ ВСЕГДА для auth кук — недоступна из JavaScript secure: true, // ❗ ВСЕГДА в production — только HTTPS sameSite: 'strict', // Защита от CSRF (strict/lax/none)
// sameSite: 'strict' — кука не отправляется при переходе с другого сайта // sameSite: 'lax' — кука не отправляется для POST с другого сайта // sameSite: 'none' — отправляется всегда (нужен secure: true)
maxAge: 24 * 60 * 60 * 1000, // 1 день (вместо expires) path: '/', domain: process.env.COOKIE_DOMAIN || undefined,});express-session — серверные сессии
Заголовок раздела «express-session — серверные сессии»npm install express-session connect-redis ioredisconst session = require('express-session');const Redis = require('ioredis');const RedisStore = require('connect-redis').default;
// Подключаем Redis для хранения сессийconst redisClient = new Redis(process.env.REDIS_URL);const store = new RedisStore({ client: redisClient });
app.use(session({ store, secret: process.env.SESSION_SECRET, // для подписи session ID resave: false, // не пересохранять если не изменилась saveUninitialized: false, // не создавать пустые сессии
cookie: { httpOnly: true, secure: process.env.NODE_ENV === 'production', sameSite: 'lax', maxAge: 24 * 60 * 60 * 1000, // 1 день },
// Имя куки (по умолчанию 'connect.sid') name: 'sid',}));Работа с сессией
Заголовок раздела «Работа с сессией»// Авторизация через сессиюapp.post('/login', async (req, res) => { const { email, password } = req.body; const user = await db.user.findUnique({ where: { email } });
if (!user || !await bcrypt.compare(password, user.passwordHash)) { return res.status(401).json({ error: 'Неверный email или пароль' }); }
// Сохраняем данные в сессии req.session.userId = user.id; req.session.role = user.role; req.session.loginAt = Date.now();
// Принудительно сохраняем (обычно автоматически) req.session.save((err) => { if (err) return next(err); res.json({ message: 'Вход выполнен' }); });});
// Middleware проверки сессииfunction sessionAuth(req, res, next) { if (!req.session.userId) { return res.status(401).json({ error: 'Не авторизован' }); } next();}
// Защищённый роутapp.get('/profile', sessionAuth, async (req, res) => { const user = await db.user.findUnique({ where: { id: req.session.userId } }); res.json({ data: user });});
// Выходapp.post('/logout', (req, res) => { req.session.destroy((err) => { if (err) return res.status(500).json({ error: 'Ошибка выхода' }); res.clearCookie('sid'); res.json({ message: 'Вышли из системы' }); });});
// Информация о сессииapp.get('/session-info', sessionAuth, (req, res) => { res.json({ userId: req.session.userId, role: req.session.role, loginAt: req.session.loginAt, sessionId: req.session.id, // осторожно — не отправляй в реальном API! });});Flash сообщения
Заголовок раздела «Flash сообщения»npm install connect-flashconst flash = require('connect-flash');app.use(flash());
// Установить flash сообщениеapp.post('/login', async (req, res) => { // ...логика входа... if (success) { req.flash('success', 'Добро пожаловать!'); res.redirect('/dashboard'); } else { req.flash('error', 'Неверные данные'); res.redirect('/login'); }});
// Читать flash сообщенияapp.get('/dashboard', (req, res) => { const messages = req.flash(); res.render('dashboard', { success: messages.success?.[0], error: messages.error?.[0], });});JWT vs Sessions
Заголовок раздела «JWT vs Sessions»JWT Sessions─────────────────────────────── ──────────────────────────────Stateless (нет хранилища) Stateful (Redis, БД)Масштабируется легко Нужен shared storageНельзя отозвать (до истечения) Можно отозвать немедленноПодходит для API, мобильных Подходит для веб-приложенийТокен в localStorage/header Session ID в httpOnly cookieУязвим если украдут токен Более безопасныйРекомендация:
- API для мобильных → JWT
- Веб-приложение с сервером → Sessions
- Нужна отзываемость → Sessions или JWT + blacklist в Redis
Практика
Заголовок раздела «Практика»- Настрой
cookie-parserи создай роут, устанавливающий несколько кук с разными настройками - Настрой
express-sessionс Redis хранилищем - Реализуй вход/выход через сессию
- Создай middleware
sessionAuthдля защиты роутов - Сравни: реализуй одно и то же через JWT и через сессии — почувствуй разницу