Подробный гайд Single session per user — разрешение нескольких сессий под одним аккаунтом
Переход от модели «Single session per user» (один пользователь = одна активная сессия, вход с нового устройства сбрасывает старую) к модели «Multiple sessions per user» (пользователь может быть авторизован одновременно на телефоне, планшете и ПК) — это стандартное требование для современных приложений.
Ниже представлен подробный технический гайд по архитектуре, реализации и безопасности этой функции.
1. Архитектурный выбор: Где храним сессии?
Чтобы разрешить несколько сессий, вам нужно отказаться от хранения состояния в памяти приложения (in-memory) и перейти к централизованному хранилищу.
* Вариант А: Server-side sessions (Redis / БД).
Классический подход. Сервер генерирует session_id, отдает клиенту (в cookie или header), а сам хранит данные в Redis или БД.
* Вариант Б: JWT + Refresh Tokens (БД / Redis).
Если вы используете Stateless JWT, сами по себе они не позволяют отзывать конкретные сессии. Вам обязательно нужно хранить Refresh Token (или список активных JTI - JWT ID) в БД/Redis для каждой сессии.
В этом гайде мы будем опираться на гибридный подход (БД/Redis для хранения метаданных сессий), так как он наиболее универсален.
2. Изменение схемы базы данных (Data Model)
В модели «одна сессия» у вас, скорее всего, было поле session_token прямо в таблице users. Теперь нам нужна отдельная таблица user_sessions.
Пример схемы таблицы user_sessions:
CREATE TABLE user_sessions (
id UUID PRIMARY KEY,
user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
refresh_token VARCHAR(255) UNIQUE NOT NULL, -- или session_id
device_info JSONB, -- { "browser": "Chrome", "os": "Windows", "type": "desktop" }
ip_address INET,
created_at TIMESTAMP DEFAULT NOW(),
last_active_at TIMESTAMP DEFAULT NOW(),
expires_at TIMESTAMP NOT NULL,
is_revoked BOOLEAN DEFAULT FALSE
);
-- Индекс для быстрого поиска сессий пользователя
CREATE INDEX idx_user_sessions_user_id ON user_sessions(user_id);
#### 3. Логика работы (Backend)
А. Авторизация (Login)
Было (Single Session): При логине мы удаляли все старые сессии пользователя и создавали одну новую.
-- СТАРАЯ ЛОГИКА
DELETE FROM user_sessions WHERE user_id = ?;
INSERT INTO user_sessions ...
Стало (Multiple Sessions): При логине мы просто создаем новую запись. Старые не трогаем.
-- НОВАЯ ЛОГИКА
INSERT INTO user_sessions (id, user_id, refresh_token, device_info, ip_address, expires_at)
VALUES (uuid_generate_v4(), ?, ?, ?, ?, ?);
Совет:
Парсите User-Agent на бэкенде (например, с помощью библиотеки ua-parser-js или user-agents в Python/Go), чтобы красиво отображать устройство пользователю.
Б. Проверка сессии (Middleware / Auth Guard)
При каждом запросе middleware должен проверять не просто валидность токена, но и его наличие в БД (если используется JWT) или просто искать сессию в Redis/БД.
# Псевдокод проверки сессии
def validate_session(request):
token = extract_token(request)
session = db.find_session_by_token(token)
if not session or session.is_revoked or session.expires_at < now():
raise UnauthorizedException("Invalid or expired session")
# Обновляем время последней активности (опционально, но полезно для UI)
db.update_last_active(session.id)
return get_user_by_id(session.user_id)
В. Выход (Logout)
Здесь появляется гибкость.
Вам нужно реализовать два эндпоинта:
1. Выход с текущего устройства:
UPDATE user_sessions SET is_revoked = TRUE WHERE id = ? AND user_id = ?;
2. Выход со всех устройств (Logout everywhere):
UPDATE user_sessions SET is_revoked = TRUE WHERE user_id = ?;
4. Специфика для JWT (Если вы используете токены)
Если вы используете чистый JWT без Refresh Token, разрешить несколько сессий и при этом иметь возможность их отзывать невозможно без создания "черного списка" (Blacklist), что убивает всю идею stateless.
Правильная архитектура для JWT + Multiple Sessions:
- При логине генерируется пара: короткий
Access Token(например, на 15 мин) и длинныйRefresh Token(на 30 дней). Refresh Tokenсохраняется в БД (в таблицуuser_sessions).- В
Access Tokenв payload добавляетсяjti(JWT ID), который равенidзаписи вuser_sessions. - При истечении
Access Tokenклиент идет на эндпоинт/refresh. Бэкенд проверяетRefresh Tokenв БД. Если он не отозван — выпускает новую пару токенов. - При отзыве сессии мы просто помечаем
Refresh Tokenв БД как отозванный. Следующий раз, когда клиент попытается обновитьAccess Token, ему будет отказано.
5. Ограничения и Безопасность (Security & Limits)
Разрешая множественные сессии, вы открываете дверь для злоупотреблений.
Обязательно внедрите следующие лимиты:
1. Лимит активных сессий (Max Sessions Limit):
Не позволяйте пользователю иметь 100 активных сессий. Установите лимит (например, 5 или 10 устройств).
Логика:
При создании новой сессии проверяем количество активных. Если лимит превышен, удаляем (или отзываем) самую старую сессию.
active_sessions = db.count_active_sessions(user_id)
if active_sessions >= MAX_SESSIONS_LIMIT:
oldest_session = db.get_oldest_active_session(user_id)
revoke_session(oldest_session.id)
2. Автоматическая очистка (Cron / TTL):
Если используете Redis, просто ставьте TTL на ключи сессий. Если используете SQL БД, напишите фоновую задачу (Cron), которая раз в сутки удаляет/отзывает сессии, у которых expires_at < NOW().
3. Защита от утечки Refresh Token:
Внедрите Refresh Token Rotation. При каждом обновлении токена старый Refresh Token удаляется и выдается новый. Если сервер замечает, что пытаются использовать уже отработанный (старый) Refresh Token, это значит, что токен украден. В этом случае нужно отозвать все сессии пользователя принудительно.
6. UI/UX: Управление сессиями на фронтенде
Чтобы пользователь чувствовал контроль, добавьте в настройки профиля раздел «Активные сессии» (Active Sessions).
Что отображать:
- Иконка устройства (Desktop, Mobile, Tablet).
- Название браузера и ОС (например, Chrome on Windows 10).
- IP-адрес (или хотя бы город/страну по GeoIP).
- Время последней активности или дата входа.
- Метка «Текущее устройство» (Current device).
Действия:
- Кнопка «Выйти» (Revoke) напротив каждой сессии.
- Кнопка «Выйти на всех других устройствах» (Logout from all other devices).
Важно:
При нажатии «Выйти» на текущем устройстве, фронтенд должен очистить локальное хранилище (localStorage/cookies) и редиректить на страницу логина.
Чек-лист
- [ ] Создать таблицу/коллекцию
user_sessions(илиrefresh_tokens). - [ ] Изменить логику
Login: убрать удаление старых сессий, добавить парсинг User-Agent и IP. - [ ] Изменить Middleware: добавить проверку существования сессии в хранилище.
- [ ] Реализовать эндпоинт
POST /logout(текущая сессия) иPOST /logout-all(все сессии). - [ ] Добавить лимит на максимальное количество одновременных сессий (например, 5).
- [ ] Написать фоновую задачу для очистки просроченных сессий.
- [ ] Реализовать API и UI для отображения списка активных сессий пользователю.
Мы делимся этой технической информацией, чтобы помочь вам в решении задач — используйте её с пониманием. Статья носит рекомендательный характер, поэтому, пожалуйста, применяйте описанные методы осмотрительно.