Пошук уроків, статей та іншого контенту
Реалізуємо сесійний вхід у Node.js, розглянемо зберігання сесій, завершення роботи та ротацію ідентифікаторів.
Сесійна автентифікація дає змогу серверу «пам’ятати» користувача між HTTP-запитами.
HTTP сам по собі не зберігає стан: кожен запит незалежний від попереднього. Щоб пов’язати кілька запитів з одним користувачем, застосунок використовує сесію:
Користувач надсилає логін і пароль.
Сервер перевіряє облікові дані.
Сервер створює сесію та зберігає в ній ідентифікатор користувача.
Сервер надсилає браузеру cookie із ідентифікатором сесії.
Браузер автоматично надсилає цю cookie під час наступних запитів.
Сервер знаходить сесію за ідентифікатором і визначає користувача.
У cookie зазвичай зберігається лише ідентифікатор сесії, а не пароль і не весь профіль користувача.
express-sessionДля роботи із сесіями в Express часто використовують пакет express-session.
Встановлення:
npm install express express-sessionМінімальне підключення:
const express = require('express');
const session = require('express-session');
const app = express();
app.use(
session({
secret: process.env.SESSION_SECRET || 'dev-only-secret',
resave: false,
saveUninitialized: false
})
);Основні параметри:
secret — секрет для підписування ідентифікатора сесії;
resave — чи потрібно повторно зберігати сесію, якщо вона не змінювалася;
saveUninitialized — чи потрібно зберігати сесію, до якої ще не додали дані.
Секрет не слід зберігати безпосередньо в коді production-застосунку. Його потрібно передавати через змінну середовища.
Після підключення middleware поточна сесія доступна через req.session:
req.session.userId = 42;За замовчуванням express-session створює cookie з ідентифікатором сесії. Її ім’я за замовчуванням — connect.sid, але його можна змінити.
Рекомендовані налаштування cookie:
app.use(
session({
name: 'sid',
secret: process.env.SESSION_SECRET || 'dev-only-secret',
resave: false,
saveUninitialized: false,
cookie: {
httpOnly: true,
secure: process.env.NODE_ENV === 'production',
sameSite: 'lax',
maxAge: 1000 * 60 * 60 * 24
}
})
);httpOnlyCookie з прапорцем httpOnly недоступна через JavaScript у браузері:
document.cookie;Це зменшує ризик крадіжки сесійної cookie через XSS-уразливість.
secureCookie із secure: true надсилається лише через HTTPS.
У локальній розробці зазвичай використовують HTTP, тому значення часто залежить від середовища:
secure: process.env.NODE_ENV === 'production'У production HTTPS має бути налаштований обов’язково.
sameSiteПараметр sameSite обмежує надсилання cookie під час міжсайтових запитів.
Для багатьох звичайних застосунків підходить:
sameSite: 'lax'maxAgemaxAge визначає час життя cookie у мілісекундах. Наприклад:
maxAge: 1000 * 60 * 60 * 24Це 24 години.
Важливо розрізняти:
час життя cookie у браузері;
час життя запису сесії у сховищі.
Ці значення потрібно узгоджувати під час налаштування конкретного сховища.
express-session зберігає в cookie лише ідентифікатор, а дані сесії передає до сховища.
Під час розробки пакет за замовчуванням використовує MemoryStore. Він зручний для прикладів, але не підходить для production, тому що:
дані зберігаються в пам’яті одного процесу;
сесії зникають після перезапуску;
сховище не призначене для великого навантаження;
воно не підходить для кількох екземплярів застосунку.
У production використовують зовнішнє сховище, наприклад Redis або базу даних, через сумісний із express-session store.
Зовнішнє сховище дає змогу:
зберігати сесії незалежно від процесу Node.js;
використовувати кілька екземплярів застосунку;
видаляти або відкликати сесії централізовано;
переживати перезапуск процесу без автоматичної втрати всіх сесій.
Сам ідентифікатор сесії не повинен містити конфіденційні дані. У cookie має бути випадковий ідентифікатор, а дані користувача — на сервері.
Після успішної перевірки облікових даних можна записати ідентифікатор користувача в сесію:
req.session.userId = user.id;Після цього middleware може використовувати req.session.userId, щоб перевіряти автентифікацію.
Повний приклад:
const express = require('express');
const session = require('express-session');
const app = express();
const port = 3000;
app.use(express.urlencoded({ extended: false }));
app.use(
session({
name: 'sid',
secret: process.env.SESSION_SECRET || 'dev-only-secret-change-me',
resave: false,
saveUninitialized: false,
cookie: {
httpOnly: true,
secure: process.env.NODE_ENV === 'production',
sameSite: 'lax',
maxAge: 1000 * 60 * 60 * 24
}
})
);
// Демонстраційний користувач замість реальної бази даних.
const demoUser = {
id: 1,
username: 'alice',
password: 'password123'
};
function requireAuth(req, res, next) {
if (!req.session.userId) {
return res.status(401).send('Потрібно увійти');
}
next();
}
app.get('/', (req, res) => {
if (req.session.userId) {
return res.send(
'Ви увійшли. Відкрийте /profile або виконайте POST-запит на /logout.'
);
}
res.send(`
<h1>Вхід</h1>
<form method="post" action="/login">
<label>
Ім'я:
<input name="username" required>
</label>
<br>
<label>
Пароль:
<input name="password" type="password" required>
</label>
<br>
<button type="submit">Увійти</button>
</form>
`);
});
app.post('/login', (req, res, next) => {
const { username, password } = req.body;
const isValidCredentials =
username === demoUser.username && password === demoUser.password;
if (!isValidCredentials) {
return res.status(401).send('Неправильне ім’я або пароль');
}
// Створюємо новий ідентифікатор сесії після входу.
req.session.regenerate((error) => {
if (error) {
return next(error);
}
req.session.userId = demoUser.id;
req.session.save((saveError) => {
if (saveError) {
return next(saveError);
}
res.redirect('/profile');
});
});
});
app.get('/profile', requireAuth, (req, res) => {
res.send(`Ви увійшли як користувач із ID ${req.session.userId}`);
});
app.post('/logout', requireAuth, (req, res, next) => {
req.session.destroy((error) => {
if (error) {
return next(error);
}
// Видаляємо cookie з ідентифікатором сесії у браузері.
res.clearCookie('sid', {
httpOnly: true,
secure: process.env.NODE_ENV === 'production',
sameSite: 'lax'
});
res.send('Ви вийшли із системи');
});
});
app.use((error, req, res, next) => {
console.error(error);
res.status(500).send('Внутрішня помилка сервера');
});
app.listen(port, () => {
console.log(`Сервер працює на http://localhost:${port}`);
});Для запуску:
node app.jsДемонстраційні облікові дані:
Ім’я: alice
Пароль: password123У реальному застосунку пароль не порівнюють у відкритому вигляді. Перевірка повинна виконуватися через безпечний алгоритм хешування паролів, а користувачів потрібно отримувати з бази даних.
Замість повторення перевірки в кожному обробнику використовують middleware:
function requireAuth(req, res, next) {
if (!req.session.userId) {
return res.status(401).send('Необхідна автентифікація');
}
next();
}
app.get('/private-data', requireAuth, (req, res) => {
res.json({
userId: req.session.userId,
data: 'Конфіденційні дані'
});
});Якщо сесія не містить userId, обробник захищеного маршруту не виконується.
У складнішому застосунку middleware може:
завантажити повного користувача з бази даних;
перевірити, чи не заблокований обліковий запис;
перевірити роль або дозволи;
додати користувача до req.user.
Однак сам факт наявності cookie ще не означає, що користувач має доступ до конкретної дії. Авторизацію потрібно перевіряти окремо.
Для виходу користувача потрібно виконати дві операції:
видалити сесію зі сховища;
видалити cookie з браузера.
У express-session для цього використовують:
req.session.destroy((error) => {
if (error) {
return next(error);
}
res.clearCookie('sid');
res.send('Вихід виконано');
});destroy() видаляє серверний запис сесії. Після цього навіть стара cookie не повинна давати доступ до облікового запису.
res.clearCookie() видаляє cookie з браузера. Параметри очищення, зокрема ім’я та шлях, мають відповідати параметрам, із якими cookie була створена.
Вихід потрібно виконувати через POST, а не через GET. Зміна стану застосунку не повинна відбуватися лише через відкриття URL або завантаження зображення.
Атака фіксації сесії, або session fixation, можлива, коли зловмисник заздалегідь знає ідентифікатор сесії користувача.
Якщо після входу сервер залишає той самий ідентифікатор, зловмисник може використати його після автентифікації жертви.
Тому після успішного входу потрібно створити новий ідентифікатор сесії. У express-session для цього використовується:
req.session.regenerate((error) => {
if (error) {
return next(error);
}
req.session.userId = user.id;
res.redirect('/profile');
});regenerate() створює нову сесію з новим ідентифікатором. Старий ідентифікатор більше не використовується для щойно автентифікованого сеансу.
Ротацію варто виконувати не лише після звичайного входу, а й після важливої зміни рівня довіри, наприклад:
після завершення багатофакторної автентифікації;
після переходу з анонімної сесії до автентифікованої;
після зміни облікового запису, якщо один браузер може перемикатися між користувачами.
Під час ротації дані попередньої сесії можуть бути втрачені. Тому необхідні значення потрібно записати в нову сесію після завершення regenerate().
Зазвичай express-session автоматично зберігає змінену сесію наприкінці запиту. Проте під час важливих переходів, особливо одразу після regenerate(), корисно явно викликати req.session.save():
req.session.regenerate((error) => {
if (error) {
return next(error);
}
req.session.userId = user.id;
req.session.save((saveError) => {
if (saveError) {
return next(saveError);
}
res.redirect('/profile');
});
});Це гарантує, що запис буде збережений до надсилання відповіді та перенаправлення.
Для сесійної автентифікації дотримуйтеся таких правил:
використовуйте довгий випадковий secret;
зберігайте секрет у змінних середовища;
встановлюйте httpOnly: true;
використовуйте secure: true у production через HTTPS;
налаштовуйте sameSite;
не зберігайте пароль у сесії;
не зберігайте конфіденційні дані у cookie;
регенеруйте ідентифікатор після входу;
знищуйте сесію під час виходу;
не використовуйте MemoryStore у production;
обмежуйте час життя сесії;
перевіряйте права доступу на сервері, а не лише в інтерфейсі.
MemoryStore у productionЦе може призвести до втрати всіх сесій після перезапуску та проблем під час роботи кількох екземплярів Node.js.
Для production потрібне зовнішнє сховище сесій.
Не варто копіювати до сесії весь об’єкт користувача. Дані можуть застаріти або містити зайву конфіденційну інформацію.
Зазвичай достатньо зберігати:
req.session.userId = user.id;А актуальні дані користувача за потреби завантажувати із бази даних.
Якщо після входу залишається старий ідентифікатор сесії, застосунок може бути вразливим до session fixation.
Використовуйте req.session.regenerate() після успішної автентифікації.
Якщо видалити cookie, але не знищити серверну сесію, запис може залишитися у сховищі. Потрібно виконувати обидві дії:
req.session.destroy(/* ... */);
res.clearCookie('sid');Якщо під час створення та очищення використовуються різні ім’я, path або інші важливі параметри, браузер може не видалити потрібну cookie.
Запит GET /logout може бути виконаний випадково або стороннім ресурсом. Для завершення сесії використовуйте POST.
Сесія пов’язує запити браузера з автентифікованим користувачем.
У cookie зберігається ідентифікатор, а дані сесії — на сервері.
express-session надає доступ до сесії через req.session.
Ідентифікатор потрібно регенерувати після успішного входу.
Для виходу потрібно знищити серверну сесію та очистити cookie.
MemoryStore підходить для локальних прикладів, але не для production.
Параметри httpOnly, secure, sameSite і час життя cookie є важливою частиною захисту.
Сесійна автентифікація та перевірка прав доступу — різні завдання: сесія визначає користувача, а middleware або інша логіка визначає, що йому дозволено.