Антон Ларичев

Введение
Аутентификация отвечает на вопрос «кто вы?», а авторизация — «что вам разрешено?». Эти два слоя часто путают, но проектируются они по-разному: первый проверяет подлинность пользователя, второй — его права. В Node.js для решения этих задач исторически сложились три подхода: серверные сессии, JWT и делегированная авторизация через OAuth2. Каждый имеет свою область применимости, и выбор зависит от модели угроз, архитектуры и удобства эксплуатации.
В статье разберём, как реализовать каждый подход на Express, где у них слабые места и как их комбинировать. Примеры намеренно минималистичные — чтобы сосредоточиться на сути, а не на обвязке.
Серверные сессии
Классическая схема: сервер хранит состояние сессии (в Redis, БД или памяти), а клиенту отдаёт только идентификатор в HttpOnly-куке. Это безопасно по умолчанию: JS на клиенте к куке не достучится, отзыв сессии — это просто удаление записи в хранилище.
import express from 'express';
import session from 'express-session';
import RedisStore from 'connect-redis';
import { createClient } from 'redis';
const redisClient = createClient({ url: process.env.REDIS_URL });
await redisClient.connect();
const app = express();
app.use(express.json());
app.use(session({
store: new RedisStore({ client: redisClient }),
secret: process.env.SESSION_SECRET,
resave: false,
saveUninitialized: false,
cookie: {
httpOnly: true, // недоступна из JS
secure: true, // только по HTTPS
sameSite: 'lax', // защита от большинства CSRF
maxAge: 1000 * 60 * 60 * 24 // сутки
}
}));
app.post('/login', async (req, res) => {
const user = await verifyCredentials(req.body.email, req.body.password);
if (!user) return res.status(401).end();
// сохраняем минимум данных — остальное подтянем из БД
req.session.userId = user.id;
res.json({ ok: true });
});
Плюсы: моментальный отзыв доступа, простая ротация, состояние всегда актуально. Минусы: нужна общая для всех инстансов сессионная БД, требуется CSRF-защита для небезопасных методов.
JWT: токены без состояния
JWT (JSON Web Token) — это подписанный токен, который сам содержит данные о пользователе. Сервер не хранит сессию: достаточно проверить подпись и срок действия. Удобно для распределённых API и микросервисов.
import jwt from 'jsonwebtoken';
const ACCESS_TTL = '15m';
const REFRESH_TTL = '30d';
function issueTokens(user) {
const access = jwt.sign(
{ sub: user.id, role: user.role },
process.env.JWT_ACCESS_SECRET,
{ expiresIn: ACCESS_TTL }
);
const refresh = jwt.sign(
{ sub: user.id, jti: crypto.randomUUID() },
process.env.JWT_REFRESH_SECRET,
{ expiresIn: REFRESH_TTL }
);
return { access, refresh };
}
// middleware для защищённых маршрутов
function authRequired(req, res, next) {
const header = req.headers.authorization ?? '';
const token = header.startsWith('Bearer ') ? header.slice(7) : null;
if (!token) return res.status(401).end();
try {
req.user = jwt.verify(token, process.env.JWT_ACCESS_SECRET);
next();
} catch {
res.status(401).end();
}
}
Главное правило — короткоживущий access-токен и отдельный refresh с возможностью отзыва. Refresh-токен лучше хранить в HttpOnly-куке и записывать его jti в БД, чтобы можно было инвалидировать конкретное устройство.
Где хранить токены на клиенте
Хранение JWT в localStorage — частый источник XSS-уязвимостей: любой инжектированный скрипт получит доступ к токену. Безопаснее держать access-токен в памяти JS-процесса, а refresh — в HttpOnly-куке с SameSite=Strict или Lax.
OAuth2: делегированная авторизация
OAuth2 нужен, когда вы хотите войти через GitHub, Google или Yandex, либо предоставить сторонним приложениям ограниченный доступ к вашему API. Это протокол получения access-токена от имени пользователя без передачи его пароля.
Для веб-приложений подходит Authorization Code Flow с PKCE. Удобнее всего реализовать его через Passport.js или специализированные библиотеки.
import passport from 'passport';
import { Strategy as GitHubStrategy } from 'passport-github2';
passport.use(new GitHubStrategy({
clientID: process.env.GITHUB_CLIENT_ID,
clientSecret: process.env.GITHUB_CLIENT_SECRET,
callbackURL: '/auth/github/callback',
scope: ['user:email']
}, async (accessToken, refreshToken, profile, done) => {
// ищем пользователя или создаём нового
const user = await upsertUserByOAuth('github', profile);
done(null, user);
}));
app.get('/auth/github', passport.authenticate('github'));
app.get('/auth/github/callback',
passport.authenticate('github', { failureRedirect: '/login' }),
(req, res) => {
// после успешного входа создаём свою сессию или JWT
req.session.userId = req.user.id;
res.redirect('/');
}
);
Обратите внимание: OAuth2 решает только задачу получения токена от провайдера. После колбэка вы всё равно создаёте собственную сессию или выдаёте свой JWT — провайдерский access-токен наружу не пробрасывается.
Частые ошибки
- Использование
alg: noneили слабого секрета для JWT. Подпись HS256 требует длинного случайного ключа, а лучше — пары RS256/ES256. - Длинный TTL у access-токена. Если токен живёт сутки, отозвать его без чёрного списка невозможно. Делайте 5–15 минут и обновляйте через refresh.
- Хранение JWT в
localStorage. Любая XSS превращается в кражу токена. Используйте HttpOnly-куки или память процесса. - Отсутствие CSRF-защиты для куки-сессий.
SameSite=Laxпомогает, но для критичных операций добавляйте CSRF-токен. - Доверие данным из JWT без проверки подписи.
jwt.decodeне проверяет подпись — всегда вызывайтеjwt.verify. - Реализация OAuth2 вручную. Протокол кажется простым, но детали PKCE, state, nonce легко упустить. Берите проверенные библиотеки.
- Смешение ролей в access-токене без актуализации. Если права пользователя поменялись, старый JWT всё ещё их содержит до истечения срока.
Заключение
Сессии, JWT и OAuth2 — не конкуренты, а инструменты с разными зонами ответственности. Для классического веб-приложения с серверным рендерингом сессии в Redis остаются самым простым и безопасным выбором. JWT раскрывают потенциал в распределённых API и мобильных клиентах, особенно в паре с refresh-токенами. OAuth2 нужен там, где требуется делегированный доступ или социальный вход.
Выбирайте подход осознанно, опираясь на модель угроз и эксплуатационные требования, и не пренебрегайте базовыми мерами: HTTPS, HttpOnly-куки, короткие TTL, проверка подписи и валидация всех входных параметров.






Комментарии
0