Пошук уроків, статей та іншого контенту
Реалізуєте сесії й cookies, налаштуєте їхні атрибути та порівняєте цей підхід із JWT.
Cookie — це невеликий фрагмент даних, який браузер зберігає для певного домену та надсилає разом із наступними HTTP-запитами.
Сесія — це стан користувача, який зберігається на сервері між запитами. Щоб сервер міг знайти цей стан, браузер отримує cookie із ідентифікатором сесії.
Типовий процес має такий вигляд:
Користувач виконує вхід.
Сервер створює сесію та зберігає в ній, наприклад, userId.
Сервер надсилає браузеру cookie із session ID.
Браузер автоматично додає цю cookie до наступних запитів.
Сервер читає session ID, знаходить сесію та розуміє, хто виконує запит.
Cookie зазвичай містить лише ідентифікатор:
sid=s%3Aabc123...Дані користувача при цьому зберігаються на сервері, а не в браузері.
NestJS за замовчуванням не створює сесії. Для застосунків на базі Express можна використати пакет express-session:
npm install express-session
npm install -D @types/express-sessionПакет підключається як middleware у main.ts.
import { NestFactory } from '@nestjs/core';
import session from 'express-session';
import { AppModule } from './app.module';
async function bootstrap() {
const app = await NestFactory.create(AppModule);
app.use(
session({
// Секрет потрібен для підпису cookie із session ID.
secret: process.env.SESSION_SECRET ?? 'development-secret-change-me',
// Не зберігати сесію повторно, якщо вона не змінилася.
resave: false,
// Не створювати сесію для запитів, які не використовують сесію.
saveUninitialized: false,
name: 'sid',
cookie: {
httpOnly: true,
secure: process.env.NODE_ENV === 'production',
sameSite: 'lax',
maxAge: 1000 * 60 * 60 * 24,
path: '/',
},
}),
);
await app.listen(3000);
}
bootstrap();Після підключення middleware кожен HTTP-запит отримує властивість req.session.
Нижче наведено мінімальний, але працездатний приклад. Він демонструє:
створення сесії після входу;
збереження userId у сесії;
перевірку поточного користувача;
знищення сесії під час виходу;
захист від фіксації сесії через regenerate().
app.controller.tsimport {
Body,
Controller,
Get,
HttpException,
HttpStatus,
Post,
Req,
Res,
} from '@nestjs/common';
import { Request, Response } from 'express';
declare module 'express-session' {
interface SessionData {
userId?: string;
}
}
type LoginBody = {
username?: string;
password?: string;
};
function regenerateSession(request: Request): Promise<void> {
return new Promise((resolve, reject) => {
request.session.regenerate((error) => {
if (error) {
reject(error);
return;
}
resolve();
});
});
}
function saveSession(request: Request): Promise<void> {
return new Promise((resolve, reject) => {
request.session.save((error) => {
if (error) {
reject(error);
return;
}
resolve();
});
});
}
function destroySession(request: Request): Promise<void> {
return new Promise((resolve, reject) => {
request.session.destroy((error) => {
if (error) {
reject(error);
return;
}
resolve();
});
});
}
@Controller()
export class AppController {
@Post('login')
async login(
@Body() body: LoginBody,
@Req() request: Request,
) {
// Для прикладу використовуються фіксовані облікові дані.
const isValid =
body.username === 'alice' && body.password === 'password';
if (!isValid) {
throw new HttpException(
'Неправильні облікові дані',
HttpStatus.UNAUTHORIZED,
);
}
// Створюємо новий session ID після успішного входу.
// Це захищає від session fixation.
await regenerateSession(request);
request.session.userId = 'user-42';
await saveSession(request);
return {
message: 'Вхід виконано',
};
}
@Get('me')
getCurrentUser(@Req() request: Request) {
if (!request.session.userId) {
throw new HttpException(
'Користувач не автентифікований',
HttpStatus.UNAUTHORIZED,
);
}
return {
userId: request.session.userId,
};
}
@Post('logout')
async logout(
@Req() request: Request,
@Res({ passthrough: true }) response: Response,
) {
await destroySession(request);
// Видаляємо cookie у браузері.
response.clearCookie('sid', {
path: '/',
});
return {
message: 'Вихід виконано',
};
}
}app.module.tsimport { Module } from '@nestjs/common';
import { AppController } from './app.controller';
@Module({
controllers: [AppController],
})
export class AppModule {}main.tsimport { NestFactory } from '@nestjs/core';
import session from 'express-session';
import { AppModule } from './app.module';
async function bootstrap() {
const app = await NestFactory.create(AppModule);
app.use(
session({
secret: process.env.SESSION_SECRET ?? 'development-secret-change-me',
resave: false,
saveUninitialized: false,
name: 'sid',
cookie: {
httpOnly: true,
secure: process.env.NODE_ENV === 'production',
sameSite: 'lax',
maxAge: 1000 * 60 * 60 * 24,
path: '/',
},
}),
);
await app.listen(3000);
}
bootstrap();Після запуску застосунку можна виконати запити:
curl -i -c cookies.txt \
-H "Content-Type: application/json" \
-d '{"username":"alice","password":"password"}' \
http://localhost:3000/loginОпція -c cookies.txt зберігає отриману cookie у файл. Наступний запит використає цю cookie:
curl -i -b cookies.txt http://localhost:3000/meВихід із системи:
curl -i -b cookies.txt -c cookies.txt \
-X POST http://localhost:3000/logoutexpress-session за замовчуванням використовує MemoryStore. Це зручно для локальної розробки, але не підходить для production.
MemoryStore має суттєві обмеження:
дані зберігаються лише в пам’яті процесу;
після перезапуску застосунку всі сесії втрачаються;
кілька екземплярів застосунку не мають спільного стану;
сховище не призначене для великого навантаження.
У production потрібно використовувати зовнішнє сховище сесій, сумісне з express-session, наприклад Redis або базу даних.
Загальна конфігурація має виглядати так:
app.use(
session({
secret: process.env.SESSION_SECRET as string,
resave: false,
saveUninitialized: false,
store: productionSessionStore,
cookie: {
httpOnly: true,
secure: true,
sameSite: 'lax',
},
}),
);Конкретний адаптер для сховища залежить від обраної інфраструктури. Важливо, щоб усі екземпляри NestJS використовували одне спільне сховище.
Атрибути cookie визначають, коли браузер зберігає cookie, коли надсилає її та чи доступна вона JavaScript-коду.
httpOnlycookie: {
httpOnly: true,
}Cookie з httpOnly не можна прочитати через document.cookie у браузері.
Це зменшує ризик викрадення session ID через шкідливий JavaScript-код. Водночас httpOnly не усуває саму вразливість XSS: шкідливий код усе ще може виконувати запити від імені користувача.
Для session ID цей атрибут потрібно використовувати майже завжди.
securecookie: {
secure: true,
}Cookie з secure надсилається лише через HTTPS.
У локальній розробці без HTTPS зазвичай використовують:
secure: process.env.NODE_ENV === 'production'У production secure має бути true.
Якщо застосунок працює за reverse proxy, наприклад за Nginx або балансувальником, Express може потребувати налаштування довіри до proxy:
const expressApp = app.getHttpAdapter().getInstance();
expressApp.set('trust proxy', 1);Це потрібно робити лише відповідно до конфігурації вашої інфраструктури.
sameSiteАтрибут sameSite обмежує надсилання cookie у cross-site-запитах.
Можливі значення:
strict — найсуворіший режим;
lax — поширений варіант для звичайної веб-автентифікації;
none — дозволяє cross-site cookie, але потребує secure: true.
Приклад:
cookie: {
sameSite: 'lax',
}SameSite допомагає захищатися від CSRF, але не є універсальною заміною повноцінним CSRF-заходам у складних сценаріях.
maxAgemaxAge задає час життя cookie у мілісекундах:
cookie: {
maxAge: 1000 * 60 * 60 * 24,
}У цьому прикладі cookie живе 24 години.
Важливо відрізняти:
час життя cookie у браузері;
час життя сесії у серверному сховищі.
Серверне сховище також має видаляти прострочені сесії.
pathcookie: {
path: '/',
}Cookie буде надсилатися для всіх шляхів домену.
Якщо cookie потрібна лише для певної частини застосунку, можна звузити область, наприклад до /admin.
domaindomain визначає домен, для якого доступна cookie. Якщо його не вказувати, браузер використовує поточний хост.
Без чіткої потреби краще не задавати широкий домен на кшталт .example.com, оскільки cookie може стати доступною для більшої кількості піддоменів.
Session middleware має бути підключений до обробників, які використовують req.session.
Правильний порядок:
app.use(session(sessionOptions));
// Після цього NestJS-контролери можуть використовувати request.session.Якщо контролер викликається до session middleware або middleware не підключений взагалі, request.session буде недоступною.
Session fixation — це атака, під час якої зловмисник намагається змусити користувача використовувати заздалегідь відомий session ID.
Тому після успішної автентифікації бажано створити новий session ID:
await regenerateSession(request);
request.session.userId = userId;
await saveSession(request);Виклик regenerate() замінює стару сесію новою. Дані, які потрібно зберегти, слід записати вже після цього виклику.
Під час виходу потрібно знищити серверну сесію:
await destroySession(request);
response.clearCookie('sid', { path: '/' });Видалення лише cookie недостатньо, якщо серверна сесія все ще залишається активною.
Сесії та JWT вирішують одну задачу — збереження стану автентифікації між запитами, але роблять це по-різному.
У cookie зберігається session ID, а дані зберігаються на сервері.
Переваги:
сервер може одразу відкликати сесію;
cookie не містить усі дані користувача;
простіше реалізувати глобальний logout;
зміна ролей або прав користувача одразу враховується сервером.
Недоліки:
потрібне серверне сховище;
у розподіленій системі всі екземпляри мають бачити спільні сесії;
кожен запит зазвичай потребує пошуку сесії у сховищі.
У токені містяться claims, наприклад ідентифікатор користувача та час завершення дії. Сервер перевіряє підпис токена.
Переваги:
сервер може перевіряти токен без пошуку сесії;
зручно для окремих сервісів та API;
токен може містити необхідний для авторизації контекст.
Недоліки:
відкликати вже виданий токен складніше;
токен може містити застарілі ролі або права;
потрібно уважно визначати строк дії та процес оновлення;
розмір токена зазвичай більший за session ID.
Сесії та JWT — це способи зберігати стан автентифікації. Cookie — це спосіб транспортувати дані між браузером і сервером.
JWT також можна зберігати в cookie, а session ID можна передавати іншим способом. Тому порівняння «cookies проти JWT» не зовсім точне: cookie і JWT належать до різних рівнів рішення.
Для класичного браузерного застосунку сесії з httpOnly cookie часто є простим і зручним варіантом. JWT може бути доречнішим, коли токени мають споживати кілька незалежних сервісів.
MemoryStore підходить для навчання та локальних тестів, але не для реального навантаження. Використовуйте спільне зовнішнє сховище.
httpOnlyЯкщо session cookie доступна JavaScript-коду, її легше викрасти у разі XSS-вразливості.
cookie: {
httpOnly: true,
}secure у productionБез secure cookie може бути надіслана через незахищене HTTP-з'єднання.
Чим довше живе сесія, тим довше зловмисник може використати викрадений session ID. Обирайте строк відповідно до типу застосунку.
У сесії достатньо зберігати мінімальний ідентифікатор, наприклад userId. Не потрібно зберігати пароль, повні дані платіжної картки або інші зайві секрети.
regenerate() після входуСтворення нового session ID після автентифікації допомагає захиститися від session fixation.
Logout має знищувати сесію на сервері та очищати cookie у браузері.
sameSiteЯкщо фронтенд і API працюють у різних site-контекстах, cookie може не надсилатися через занадто суворе значення sameSite. Якщо використовується SameSite=None, обов’язково потрібен HTTPS і secure: true.
Cookie зберігається у браузері та надсилається серверу з наступними запитами.
Сесія зберігає стан користувача на сервері.
express-session підключається у NestJS як middleware.
Для session cookie зазвичай потрібні httpOnly, secure у production та відповідний sameSite.
MemoryStore не слід використовувати у production.
Після входу варто викликати session.regenerate().
Під час logout потрібно знищити серверну сесію та очистити cookie.
Сесії дають просте відкликання доступу, тоді як JWT зменшує залежність від серверного сховища.
Cookie і JWT — різні поняття: cookie є транспортом, а JWT — форматом токена.