Сессии и контексты в Node.js
В долгоживущем Node-процессе сессия пользователя живёт в памяти десятками минут/часов. Контекст запроса нужно изолировать чтобы данные одного юзера не утекали другому через общий scope модуля.
Сессия в долгоживущем процессе
В PHP/Apache процесс пересоздаётся на каждый запрос — сессия в файле/БД, читается каждый раз. В Node всё иначе:
// Сессии прямо в Map в памяти процесса
const sessions = new Map();
function login(userId) {
const token = crypto.randomUUID;
sessions.set(token, { userId, lastSeen: Date.now() });
return token;
}
function authenticate(token) {
const s = sessions.get(token);
if (!s) return null;
s.lastSeen = Date.now(); // touch
return s;
}
// + Periodic cleanup
setInterval(() => {
const now = Date.now();
for (const [t, s] of sessions) {
if (now - s.lastSeen > 30 * 60_000) sessions.delete(t);
}
}, 60_000);
Плюсы: 0 latency на чтение, можно хранить богатый state (открытые WebSocket, кэш расчётов). Минусы: при рестарте теряется, при кластере нужна sticky session или общий Redis.
Контекст запроса (request context)
Главная угроза — утечка данных между запросами:
// ❌ ПЛОХО: общий scope модуля
let currentUser; // утекает между запросами
app.use((req, res, next) => {
currentUser = req.user; // ← race condition!
next;
});
app.get('/api/profile', () => sendProfile(currentUser));
// При параллельных запросах currentUser перетирается → user A видит данные B
// ✅ ПРАВИЛЬНО: AsyncLocalStorage
const { AsyncLocalStorage } = require('async_hooks');
const ctx = new AsyncLocalStorage();
app.use((req, res, next) => {
ctx.run({ user: req.user, traceId: req.id }, () => next);
});
function getCurrentUser() {
return ctx.getStore?.user;
}
// Каждая async chain имеет свой store, никаких пересечений
Альтернативы AsyncLocalStorage
- Пробрасывать context явно: первый аргумент каждой функции
(ctx, ...). Многословно, но прозрачно - vm.createContext на каждый запрос — изоляция полная, но дорого
- Worker thread на каждый запрос — для критичных по безопасности кейсов
Миграция сессий между процессами
При graceful shutdown воркера в cluster нужно перенести активные сессии:
- Воркер получает SIGTERM
- Перестаёт принимать новые соединения
- Сериализует сессии и отправляет в master через IPC
- Master распределяет сессии между живыми воркерами
- Воркер завершается
Подводные камни
- Общий scope модуля = глобальные переменные — никогда не клади в них request-scoped данные
- AsyncLocalStorage overhead — есть, но в большинстве приложений незаметен
req.usermixin — Express-стиль примеси удобен, но не выносить за пределы request- Redis-сессии возвращают latency — компромисс между производительностью и масштабированием
- JWT не сессия — JWT stateless, нет invalidation; настоящие сессии нужны для logout/ban
🎓 Источники
- 🎓 [Sessions and contexts on Node.js and Metarhia] · 2021-02-05 · YouTube · [Marp](../../../Documents/TimurShemsedinov/2021-02-05 — 💻 Sessions and contexts on Node.js and the Metarhia tech stack (5u8imY9SJiQ).md)
- Тезисы: сессия в памяти долгоживущего процесса, изоляция per-request, миграция при шатдауне
- 🎓 [Безопасность приложений Node.js] · 2019-11-28 · YouTube
- Тезисы: общая память запросов — главная уязвимость Node по сравнению с PHP, mixin к req/res провоцирует утечки, коннект перешёл к другому юзеру
- Цитата: «Глобальное состояние и data race — главная причина утечек данных в Node»
- 🎓 [Введение в Node js и серверный JavaScript] · 2019-11-16 · YouTube
- Тезисы: сессия пользователя живёт долго; миграция сессий между процессами при scale; в Node не нужен REST т.к. сессия в памяти