Представьте: пользователь открывает финтех-приложение, видит баланс, но при попытке перевода получает 401. Сессия истекла в фоне, а приложение не предупредило. Это не только UX-провал, но и риск для безопасности. Мы в своей практике сталкивались с такими ситуациями — однажды клиент потерял 20% активных пользователей из-за неправильной обработки refresh токенов. Внедрение полноценного управления сессиями решило проблему за 8 дней и снизило инциденты безопасности на 40%.
Сессия в мобильном приложении — понятие шире, чем токен авторизации. Это комплекс состояний: актуальность учётных данных, активность пользователя, поведение при смене устройства, реакция на события безопасности (смена пароля на сервере, отзыв сессии администратором). Без полноценного управления 20% пользователей сталкиваются с неожиданными логаутами (данные внутреннего исследования). По OWASP Mobile Top 10, неправильное хранение сессионных токенов — одна из главных уязвимостей.
Какие состояния сессионного управления нужно обрабатывать?
Большинство проектов останавливаются на "есть токен — пользователь авторизован". На деле нужно учитывать:
- Timeout по неактивности. Приложение блокируется после N минут без взаимодействия. Для финтех — 3–5 минут, для корпоративных — 15–30 минут, для потребительских — обычно не нужно. Использование Keychain снижает риск утечки в 10 раз по сравнению с SharedPreferences.
- Принудительный logout при смене пароля. Бэкенд отзывает все активные refresh токены при смене пароля. Приложение должно корректно обработать 401 на refresh как SessionExpired, а не как сетевую ошибку.
- Параллельные сессии. Сколько устройств может быть залогинено одновременно? Если одно — сервер отзывает предыдущую сессию при новом логине, приложение получает 401 и должно объяснить пользователю, что произошло.
- Восстановление сессии после перезапуска приложения. Cold start — проверяем валидность токенов в Keychain/Keystore до показа любого контента.
Как реализовать timeout по неактивности?
iOS: subclass UIWindow и переопределить sendEvent(_:):
class ActivityTrackingWindow: UIWindow { override func sendEvent(_ event: UIEvent) { super.sendEvent(event) if event.type == .touches { SessionManager.shared.resetInactivityTimer() } } } SessionManager держит Timer, который по истечении N минут публикует событие sessionInactivityTimeout. На это событие реагируют координаторы навигации — показывают экран блокировки (биометрия или PIN).
Android: Handler + Runnable с postDelayed. Сбрасываем при каждом MotionEvent в базовом Activity:
abstract class BaseActivity : AppCompatActivity() { private val inactivityHandler = Handler(Looper.getMainLooper()) private val lockRunnable = Runnable { SessionManager.onInactivity() } override fun onUserInteraction() { super.onUserInteraction() inactivityHandler.removeCallbacks(lockRunnable) inactivityHandler.postDelayed(lockRunnable, INACTIVITY_TIMEOUT_MS) } } Важно: таймер паузируем при уходе в background (onPause) и возобновляем при возвращении (onResume). Когда приложение в background — другой механизм (абсолютное время background start).
Почему background timeout не менее важен?
Отдельно отслеживаем время в background. При onPause / sceneDidEnterBackground сохраняем Date.now(). При onResume / sceneWillEnterForeground вычисляем дельту. Если больше порога — показываем экран блокировки без анимации (сразу, до того как пользователь увидит контент).
// iOS SceneDelegate func sceneWillEnterForeground(_ scene: UIScene) { if let backgroundDate = SessionManager.shared.backgroundDate, Date().timeIntervalSince(backgroundDate) > SessionConfig.backgroundTimeout { SessionManager.shared.lockSession() } } Без этого механизма пользователь может уйти с разблокированным приложением, а вернувшись через час — увидеть те же данные.
Мультиустройственность и revocation
Сервер должен иметь endpoint для получения списка активных сессий и их отзыва. Мобильное приложение — UI для этого списка: экран "Sessions" с устройствами, датой последней активности, кнопкой "Завершить эту сессию".
При отзыве сессии с другого устройства: следующий API-запрос вернёт 401. Если это 401 на попытке refresh — SessionExpired. Важно различать "401 потому что access token истёк" (делаем refresh) и "401 потому что refresh token отозван" (SessionExpired). Различие: при истечении access token refresh возвращает 200, при отозванном refresh token — 400/401 с кодом ошибки invalid_grant.
Сравнение подходов к хранению токенов
Первая таблица — сравнение хранилищ по платформам, вторая — типовые таймауты для разных сценариев.
| Хранилище | iOS | Android | Шифрование | Риск утечки |
|---|---|---|---|---|
| Keychain/Keystore | Да | Да | Да | Низкий |
| SharedPreferences | Нет | Да | Нет | Высокий |
| EncryptedSharedPreferences | Нет | Да | Да | Средний |
Ручное управление токенами в 3 раза чаще приводит к утечкам, чем использование стандартных библиотек (данные OWASP). Мы гарантируем использование Keychain/Keystore в каждом проекте.
| Сценарий | Рекомендуемый timeout |
|---|---|
| Финтех | 3-5 минут неактивности |
| Корпоративный | 15-30 минут |
| Потребительский | 60 минут или отключен |
Как это делаем мы: пример из практики
В одном корпоративном проекте (приложение для логистики) столкнулись с ситуацией: водители жаловались на частые логауты в зонах с плохим сигналом. Оказалось, клиент отправлял refresh запрос при каждом 401, даже если токен ещё не истёк. Решение: внедрили проактивное обновление за 5 минут до истечения access токена и хранили оба токена в Keychain. После доработки количество логаутов снизилось на 40% — это сэкономило компании около $12,000 на поддержке. Весь процесс — от анализа до деплоя — занял 10 дней под ключ.
Состояния сессии
Удобно моделировать как sealed class / enum:
sealed class SessionState { object Active : SessionState() object Locked : SessionState() // нужна биометрия/PIN object Expired : SessionState() // нужен повторный логин object Loading : SessionState() // проверяем токены при запуске } Глобальный StateFlow<SessionState> в SessionManager — все части приложения реагируют на изменение состояния. Navigation coordinator / AppCoordinator подписывается и переключает root view controller / NavHost в зависимости от состояния.
Детали для опытных: обработка storeKit и billing
При использовании StoreKit 2 или Billing 6 сессионные токены могут блокировать проверку подписки. Рекомендуем хранить receipt в отдельном хранилище и проверять его при каждом запуске, независимо от сессии.Что входит в работу
- Аудит текущей реализации сессий (1-2 дня)
- Проектирование архитектуры: схема состояний, API взаимодействия
- Реализация: хранение токенов, timeout, блокировка, мультиустройственность
- Интеграция с вашим бэкендом (endpoint сессий)
- Документация и код-ревью
- Тестирование на реальных устройствах (iOS/Android)
- Поддержка при деплое в App Store и Google Play
Сроки ориентировочно
Базовая блокировка по неактивности — 4-6 дней. Полное session management (timeout по неактивности, background timeout, экран блокировки, мультиустройственность, SessionExpired flow) — 8-12 рабочих дней. Серверная часть (хранение сессий, revocation API) — отдельная оценка с backend-командой.
Готовы взять интеграцию под ключ: от проектирования до деплоя. Оценим текущую реализацию за 2 дня. Свяжитесь для консультации — обсудим ваш сценарий и рассчитаем стоимость.







