Уявіть: користувач відкриває фінтех-додаток, бачить баланс, але при спробі переказу отримує 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 дні. Зв'яжіться для консультації — обговоримо ваш сценарій і розрахуємо вартість.







