Реализация Refresh Token механизма в мобильном приложении

TRUETECH занимается разработкой, поддержкой и обслуживанием мобильных приложений iOS, Android, PWA. Имеем большой опыт и экспертизу для публикации мобильных приложений в популярные маркеты Google Play, App Store, Amazon, AppGallery и другие.
Разработка и поддержка любых видов мобильных приложений:
Информационные и развлекательные мобильные приложения
Новостные приложения, игры, справочники, онлайн-каталоги, погодные, фитнес и здоровье, туристические, образовательные, социальные сети и мессенджеры, квиз, блоги и подкасты, форумы, агрегаторы
Мобильные приложения электронной коммерции
Интернет-магазины, B2B-приложения, маркетплейсы, онлайн-обменники, кэшбэк-сервисы, биржи, дропшиппинг-платформы, программы лояльности, доставка еды и товаров, платежные системы
Мобильные приложения для управления бизнес-процессами
CRM-системы, ERP-системы, управление проектами, инструменты для команды продаж, учет финансов, управление производством, логистика и доставка, управление персоналом, системы мониторинга данных
Мобильные приложения электронных услуг
Доски объявлений, онлайн-школы, онлайн-кинотеатры, платформы предоставления электронных услуг, платформы кешбека, видеохостинги, тематические порталы, платформы онлайн-бронирования и записи, платформы онлайн-торговли

Это лишь некоторые из типы мобильных приложений, с которыми мы работаем, и каждый из них может иметь свои специфические особенности и функциональность, а также быть адаптированным под конкретные потребности и цели клиента.

Предлагаемые услуги
Показано 1 из 1 услугВсе 1735 услуг
Реализация Refresh Token механизма в мобильном приложении
Средняя
~1 рабочий день
Часто задаваемые вопросы
Наши компетенции:
Этапы разработки
Последние работы
  • image_mobile-applications_feedme_467_0.webp
    Разработка мобильного приложения для компании FEEDME
    756
  • image_mobile-applications_xoomer_471_0.webp
    Разработка мобильного приложения для компании XOOMER
    624
  • image_mobile-applications_rhl_428_0.webp
    Разработка мобильного приложения для компании RHL
    1054
  • image_mobile-applications_zippy_411_0.webp
    Разработка мобильного приложения для компании ZIPPY
    947
  • image_mobile-applications_affhome_429_0.webp
    Разработка мобильного приложения для компании Affhome
    874
  • image_mobile-applications_flavors_409_0.webp
    Разработка мобильного приложения для компании FLAVORS
    445

Реализация Refresh Token механизма в мобильном приложении

Refresh token — самая недооценённая часть auth-системы. Access token заканчивается — пользователь не должен этого замечать. На практике половина проблем "приложение разлогинивает" происходит именно от неправильного refresh-механизма.

Три сценария, которые ломают naive-реализацию

Гонка запросов. Пользователь открывает экран — приложение параллельно запускает три API-вызова. Все три получают 401 (access token истёк). Все три запускают refresh. Первый refresh успешно обновляет токены. Второй отправляет уже использованный refresh token — при Refresh Token Rotation сервер его отзывает как подозрительный. Третий то же самое. Итог: пользователь принудительно выходит из системы, хотя access token реально истёк только что.

Refresh в фоне. iOS BackgroundTasks или Android WorkManager запускают синхронизацию данных в фоне. В этот момент основное приложение тоже делает refresh. Два параллельных refresh с одним токеном — классическая проблема при Rotation.

Истёкший refresh token. Пользователь не открывал приложение 30 дней. Refresh token тоже истёк. Приложение делает тихий refresh → получает 401/400 → должно корректно перейти на экран логина, а не зациклиться на бесконечных запросах.

Правильная архитектура

Единственный источник правды о токенах — TokenRepository (или AuthRepository). Никакой компонент кроме него не читает и не пишет токены напрямую.

Refresh вызывается только через TokenRepository.getValidAccessToken(). Внутри — mutex или actor-изоляция:

// Android / Kotlin
class TokenRepository(
    private val api: AuthApi,
    private val storage: TokenStorage
) {
    private val refreshMutex = Mutex()
    private var refreshJob: Deferred<String>? = null

    suspend fun getValidAccessToken(): String {
        val current = storage.getAccessToken()
        if (current != null && !current.isExpired()) return current

        return refreshMutex.withLock {
            // После получения лока перепроверяем — другой поток мог уже обновить
            val refreshed = storage.getAccessToken()
            if (refreshed != null && !refreshed.isExpired()) return@withLock refreshed

            val newTokens = api.refresh(storage.getRefreshToken()
                ?: throw SessionExpiredException())
            storage.saveTokens(newTokens)
            newTokens.accessToken
        }
    }
}

Double-checked locking внутри mutex — обязательно. Иначе все потоки, дождавшиеся лока, снова делают refresh.

На iOS с Swift Concurrency — actor:

actor TokenStore {
    private var isRefreshing = false
    private var waiters: [CheckedContinuation<String, Error>] = []

    func getValidToken(refresher: AuthService) async throws -> String {
        let stored = storage.accessToken
        if let token = stored, !token.isExpired { return token.value }

        if isRefreshing {
            return try await withCheckedThrowingContinuation { waiters.append($0) }
        }

        isRefreshing = true
        do {
            let tokens = try await refresher.refresh(storage.refreshToken)
            storage.save(tokens)
            waiters.forEach { $0.resume(returning: tokens.accessToken) }
            waiters.removeAll()
            isRefreshing = false
            return tokens.accessToken
        } catch {
            waiters.forEach { $0.resume(throwing: error) }
            waiters.removeAll()
            isRefreshing = false
            throw error
        }
    }
}

Хранение Refresh Token

Refresh token — самый чувствительный секрет. Живёт дольше, даёт больше прав (получить новый access token).

  • iOS: Keychain с kSecAttrAccessibleAfterFirstUnlock (доступен после первой разблокировки, включая фоновые операции) или kSecAttrAccessibleWhenUnlocked (только при разблокированном экране, если фон не нужен).
  • Android: EncryptedSharedPreferences через MasterKey из Android Keystore.

Никогда не логируем refresh token. Проверяем, что Crashlytics, Sentry и другие SDK не захватывают HTTP-запросы с refresh token в теле. В OkHttp — кастомный Interceptor с маскировкой sensitive headers/body перед передачей в crashlytics.

Refresh Token Rotation

Если сервер поддерживает Rotation: каждый успешный refresh возвращает новый refresh token, старый инвалидируется. Это ограничивает окно атаки при компрометации токена.

Последствие для мобилки: нельзя сохранить "второй" refresh token как резервный. Всегда работаем с одним, атомарно сохраняем новую пару после refresh.

Обработка SessionExpired

Когда refresh token истёк или отозван — пользователю надо сообщить и перевести на экран логина. Делаем это через глобальный event bus или Notification/Flow:

// Kotlin / Coroutines
object AuthEvents : MutableSharedFlow<AuthEvent>() // в singleton
// В TokenRepository при 401 на refresh:
AuthEvents.emit(AuthEvent.SessionExpired)
// В Activity/Fragment:
lifecycleScope.launch {
    AuthEvents.collect { if (it == AuthEvent.SessionExpired) navigateToLogin() }
}

Не показываем стандартный системный alert — это наш UX, объясняем пользователю, что сессия завершена.

Сроки

Реализация правильного refresh механизма с mutex/actor-изоляцией, корректным хранением, обработкой SessionExpired и покрытием unit-тестами (включая тест гонки запросов) — 4–8 рабочих дней. Если добавляется поддержка фоновых задач (WorkManager / BackgroundTasks) — ещё 2–3 дня.