Logo Craft Homelab Docs Контакты Telegram
Аутентификация: сессии vs токены — сравнение подходов
Thu Dec 11 2025

Сессии и токены: выбор подхода к аутентификации

Выбор между сессиями и токенами — важное архитектурное решение. Рассмотрим преимущества и недостатки каждого подхода. Сессионная аутентификация существует десятилетиями, тогда как токен-ориентированный подход стал популярен с ростом SPA и мобильных приложений.

Server-Side Sessions

Сессионная аутентификация — классический подход, используемый десятилетиями. Состояние хранится на сервере, клиент получает только идентификатор сессии.

Как работает

Процесс аутентификации через сессии включает несколько шагов. Сервер хранит состояние в Redis или базе данных.

1. Пользователь отправляет credentials
2. Сервер создаёт сессию, сохраняет в storage (Redis/БД)
3. Сервер отправляет session ID в cookie
4. При каждом запросе сервер проверяет сессию

Реализация (Express + Redis)

const express = require('express');
const session = require('express-session');
const RedisStore = require('connect-redis').default;
const redis = require('redis');

const app = express();
const redisClient = redis.createClient();

app.use(session({
    store: new RedisStore({ client: redisClient }),
    secret: process.env.SESSION_SECRET,
    name: 'sessionId',
    resave: false,
    saveUninitialized: false,
    cookie: {
        secure: true,        // HTTPS only
        httpOnly: true,      // JS не может читать
        maxAge: 24 * 60 * 60 * 1000, // 24 часа
        sameSite: 'strict'
    }
}));

app.post('/login', async (req, res) => {
    const { email, password } = req.body;
    const user = await validateUser(email, password);
    
    if (user) {
        req.session.userId = user.id;
        req.session.role = user.role;
        res.json({ success: true });
    } else {
        res.status(401).json({ error: 'Invalid credentials' });
    }
});

app.get('/profile', (req, res) => {
    if (!req.session.userId) {
        return res.status(401).json({ error: 'Not authenticated' });
    }
    res.json({ userId: req.session.userId, role: req.session.role });
});

app.post('/logout', (req, res) => {
    req.session.destroy();
    res.json({ success: true });
});

Хранение сессий

# Redis структура
session:{sessionId} -> {
    userId: "123",
    role: "admin",
    loginAt: 1234567890,
    ip: "192.168.1.1",
    userAgent: "Mozilla/..."
}

# TTL - автоматическое удаление

Преимущества сессий

  • Мгновенная инвалидация — мгновенный logout
  • Централизованное хранилище — один source of truth
  • Встроенная защита от XSS — httpOnly cookies
  • Простота отладки — видим все активные сессии

Недостатки сессий

  • Требует stateful storage — Redis/БД
  • Масштабирование — sticky sessions или shared storage
  • Задержка — каждый запрос к storage

JWT Tokens

JWT-аутентификация — stateless подход, где токен содержит всю необходимую информацию о пользователе.

Как работает

В отличие от сессий, JWT не требует хранения состояния на сервере. Токен подписывается и может быть верифицирован без обращения к базе данных.

1. Пользователь отправляет credentials
2. Сервер подписывает JWT с user data
3. Клиент хранит токен (localStorage/cookie)
4. При каждом запросе сервер верифицирует подпись

Реализация

const jwt = require('jsonwebtoken');
const crypto = require('crypto');

app.post('/login', async (req, res) => {
    const { email, password } = req.body;
    const user = await validateUser(email, password);
    
    if (user) {
        const accessToken = jwt.sign(
            { sub: user.id, role: user.role },
            process.env.JWT_SECRET,
            { expiresIn: '15m' }
        );
        
        const refreshToken = crypto.randomBytes(64).toString('hex');
        await saveRefreshToken(user.id, refreshToken);
        
        res.cookie('refreshToken', refreshToken, {
            httpOnly: true,
            secure: true,
            sameSite: 'strict',
            maxAge: 7 * 24 * 60 * 60 * 1000
        });
        
        res.json({ accessToken });
    } else {
        res.status(401).json({ error: 'Invalid credentials' });
    }
});

app.get('/profile', authenticate, (req, res) => {
    res.json({ userId: req.user.sub, role: req.user.role });
});

// Middleware
const authenticate = (req, res, next) => {
    const authHeader = req.headers.authorization;
    const token = authHeader?.split(' ')[1];
    
    if (!token) return res.status(401).json({ error: 'No token' });
    
    try {
        req.user = jwt.verify(token, process.env.JWT_SECRET);
        next();
    } catch (err) {
        res.status(401).json({ error: 'Invalid token' });
    }
};

// Refresh
app.post('/refresh', async (req, res) => {
    const refreshToken = req.cookies.refreshToken;
    
    const stored = await getRefreshToken(refreshToken);
    if (!stored || stored.expiresAt < Date.now()) {
        return res.status(401).json({ error: 'Invalid refresh token' });
    }
    
    const accessToken = jwt.sign(
        { sub: stored.userId },
        process.env.JWT_SECRET,
        { expiresIn: '15m' }
    );
    
    res.json({ accessToken });
});

// Logout
app.post('/logout', async (req, res) => {
    const refreshToken = req.cookies.refreshToken;
    if (refreshToken) {
        await deleteRefreshToken(refreshToken);
    }
    res.clearCookie('refreshToken');
    res.json({ success: true });
});

Преимущества токенов

  • Stateless — не требует central storage
  • Масштабирование — любой сервер может верифицировать
  • Производительность — проверка подписи быстрее БД
  • Cross-platform — работает с мобильными и SPA

Недостатки токенов

  • Сложность отзыва — требует blacklist или short-lived
  • Размер — токен передаётся с каждым запросом
  • Безопасность — токены в localStorage уязвимы к XSS

Сравнение

Сессии:

  • Storage: Redis/БД
  • Масштабирование: Требует shared storage
  • Logout: Мгновенный
  • Размер запроса: Small cookie
  • Сложность: Проще
  • Mobile API: Неудобно
  • SSR: Отлично

JWT:

  • Storage: Нет (stateless)
  • Масштабирование: Простое горизонтальное
  • Logout: Требует blacklist
  • Размер запроса: Larger token
  • Сложность: Сложнее (refresh, blacklist)
  • Mobile API: Удобно
  • SSR: Требует cookie

Гибридный подход

// Access token - короткоживущий JWT
const accessToken = jwt.sign(
    { sub: user.id, role: user.role, jti: uuid() },
    process.env.JWT_SECRET,
    { expiresIn: '15m' }
);

// Refresh token - secure httpOnly cookie
res.cookie('refreshToken', refreshToken, {
    httpOnly: true,
    secure: true,
    sameSite: 'strict',
    maxAge: 7 * 24 * 60 * 60 * 1000
});

// Invalidation - добавляем jti в blacklist
const isTokenRevoked = async (jti) => {
    return await redisClient.exists(`revoked:${jti}`);
};

Когда что использовать

Используйте сессии когда:

  • Монолитное приложение
  • Простая архитектура
  • Требуется мгновенный logout
  • Single server или shared Redis

Используйте JWT когда:

  • Микросервисная архитектура
  • Mobile/SPA приложения
  • Требуется cross-domain auth
  • Высокая нагрузка

Security Best Practices

Для сессий

app.use(session({
    secret: process.env.SESSION_SECRET,
    name: 'sid',                    // Не используйте default connect.sid
    cookie: {
        secure: true,               // HTTPS only
        httpOnly: true,             // Защита от XSS
        sameSite: 'strict',         // CSRF protection
        maxAge: 24 * 60 * 60 * 1000
    },
    resave: false,
    saveUninitialized: false
}));

Для JWT

// 1. Short-lived access tokens
jwt.sign(payload, secret, { expiresIn: '15m' });

// 2. Secure storage - httpOnly cookie
res.cookie('token', token, {
    httpOnly: true,
    secure: true,
    sameSite: 'strict'
});

// 3. Refresh token rotation
// Генерируйте новый refresh token при каждом обновлении

// 4. Blacklist для критических операций

Заключение

Выбор между сессиями и токенами зависит от архитектуры. Для современных SPA и микросервисов — JWT с refresh token rotation. Для традиционных приложений — сессии.