20 апр. 2025 г.·8 мин чтения

Общие сценарии авторизации для приложений React, Swift и Kotlin

Общие сценарии авторизации помогают согласовать обновление токенов, выход из аккаунта и доверие к устройству в приложениях на React, Swift и Kotlin без общего кода.

Общие сценарии авторизации для приложений React, Swift и Kotlin

Почему поведение авторизации расходится между клиентами

Рассинхрон авторизации начинается со времени. Команды web, iOS и Android редко выпускают изменения в один и тот же день и проверяют их по-разному. Одна команда исправляет ошибку обновления токена на этой неделе, другая откладывает её на следующий спринт, а третья быстро делает временный патч, потому что релиз в магазин занимает больше времени.

Этот разрыв кажется небольшим, но пользователи ощущают его быстро. Человек входит в аккаунт на web и остаётся в системе на несколько дней, а потом открывает мобильное приложение и снова видит запрос на вход. После этого он перестаёт доверять приложению, а не правилам авторизации.

Небольшие изменения в правилах тоже накапливаются. Один клиент обновляет токен за 2 минуты до истечения срока. Другой ждёт ответа 401. Третий клиент очищает локальные данные после одной неудачной попытки обновления, а остальные делают ещё один повтор. В коде это мелкие решения, но из одного и того же аккаунта они дают разные результаты.

Хороший пример — пользователь, который входит в систему на ноутбуке, iPhone и Android-планшете. Браузер тихо обновляет данные в фоне. iPhone снова просит Face ID, потому что истёк таймер доверия к устройству. Приложение Android выходит из аккаунта после пробуждения, потому что сразу считает старый токен недействительным. Для пользователя общие сценарии авторизации выглядят случайными.

Команды поддержки обычно замечают проблему раньше инженеров. Сначала появляются обращения вроде «Я всё время вылетаю из системы» или «Почему одно устройство снова просит войти, а другое нет?». Разобрать такие сообщения трудно, потому что у каждого клиента своя маленькая версия правил.

Авторизация расходится, когда команды разделяют цели, но не точное поведение. Если вход, обновление, выход и правила доверия к устройству описаны только неявно, каждый клиент заполняет пробелы по-своему. Так простая система авторизации превращается в три разных опыта.

Что должно совпадать во всех клиентах

Приложения на React, Swift и Kotlin не обязаны использовать один и тот же код. Им нужны одни и те же правила. Если одно приложение оставляет пользователя в системе на часы после того, как сервер завершил сессию, это быстро становится заметно. Так и ломаются общие сценарии авторизации.

Начните с небольшого контракта, которому следует каждый клиент. Сделайте его настолько простым, чтобы frontend-разработчик, iOS-разработчик, Android-разработчик и backend-команда понимали его одинаково.

Согласуйте поведение, а не реализацию

Пользователь считается вошедшим в систему, когда приложение может прямо сейчас вызывать защищённые API и у него есть рабочий путь, чтобы остаться в системе. На практике это обычно значит, что access token работает, а у приложения ещё есть refresh token или серверная сессия, которую backend принимает. Не позволяйте одному клиенту считать экран с кэшированным профилем доказательством входа, а другому требовать живой токен.

Срок действия сессии тоже должен иметь одно значение. Определите точный момент, когда сессия заканчивается. Часто правило простое: сессия истекла, если обновление не удалось, потому что refresh token недействителен, отозван, отсутствует или вышел за допустимый возраст. Не тогда, когда интерфейс выглядит устаревшим. Не тогда, когда фоновой запрос завис по таймауту.

Используйте и одно правило принудительного выхода. Если приложение пытается обновиться, а сервер говорит, что сессии больше нет, все клиенты должны делать одно и то же: очистить сохранённые данные авторизации, сбросить состояние пользователя и перевести его на экран входа. Не пытайтесь на одной платформе сделать пять разных повторов, а на другой сразу выводить из аккаунта. Это рождает странные ошибки и обращения в поддержку.

Правила доверия к устройству тоже должны быть названы простыми словами. Сложные ярлыки заставляют команды спорить о крайних случаях. Простые названия работают лучше:

  • Новое устройство — пользователь вошёл, но этому устройству нужна дополнительная проверка
  • Доверенное устройство — пользователь прошёл проверки, обычный доступ разрешён
  • Заблокированное устройство — это устройство не может продолжить работу и должно выйти из системы

Этого достаточно для большинства продуктов. Если нужна большая детализация, добавьте серверные поля за этими названиями, а не новые ярлыки в каждом клиенте.

Хорошее правило такое: сервер определяет истину авторизации, а каждое приложение следует тем же результатам. Приложение React может использовать interceptors, приложение Swift — async tasks, а приложение Kotlin — OkHttp authenticator. Код может отличаться. Результат — нет.

Сначала напишите контракт авторизации

Для общих сценариев авторизации контракт важнее общего кода. Если клиенты на React, Swift и Kotlin следуют одним и тем же правилам на бумаге, они могут вести себя одинаково, даже если реализация выглядит по-разному.

Начните с сетевых правил. Запишите каждый auth endpoint, форму запроса, ожидаемые коды ответов и то, что клиент должен делать дальше. Ответ 401 при обычном API-запросе может запускать одну попытку обновления. Ответ 401 от endpoint для refresh должен завершать сессию. Ответ 429 должен означать ожидание и повтор в пределах фиксированного лимита, а не бесконечный цикл до падения приложения.

Потом чётко назовите все токены. Команды часто путаются, когда один клиент использует session token как access token, а мобильное приложение хранит что-то, чем web-приложение вообще не пользуется. Контракт должен говорить, какие токены существуют, за что отвечает каждый из них, сколько он живёт и где каждый клиент его хранит. Описывайте это общими словами: «память», «cookie» или «защищённое хранилище устройства». Не привязывайте Keychain, Keystore или выбор браузерной библиотеки к общему контракту.

Сроки обновления тоже требуют такой же детализации. Запишите окно обновления, период льготы и правило для расхождения часов. Например, если access token живёт 15 минут, можно обновлять его, когда остаётся меньше 2 минут, и разрешить короткий льготный период для запросов, которые уже ушли. Такая мелочь помогает избежать случайных выходов из аккаунта.

Хороший контракт авторизации обычно отвечает на четыре простых вопроса:

  • Что возвращает каждый auth endpoint при успехе и при ошибке?
  • Какой токен клиент отправляет, обновляет, хранит или удаляет?
  • Когда клиент повторяет попытку, а когда останавливается?
  • Какие правила общие для всех платформ, а какие детали хранилища остаются локальными?

Многие команды пропускают именно этот шаг, потому что он кажется скучным. Это не так. Короткий контракт экономит дни отладки позже, особенно когда web- и mobile-команды выпускают изменения по разным графикам.

Шаг за шагом стандартизируйте обновление токенов

Рассинхрон обновления обычно начинается в мелочах. Web-приложение обновляет токен только после 401, iPhone делает это за минуту до истечения срока, а Android повторяет попытку дважды, потому что кому-то это казалось безопаснее. Сценарий общих потоков авторизации работает лучше, когда каждый клиент следует одним и тем же правилам, даже если код выглядит по-разному.

Начните с одного простого числа: насколько близко к истечению уже считается слишком близко. Например, если access token истекает меньше чем через 60 секунд, все клиенты должны обновлять его до отправки защищённого запроса. Это правило легко перенести в React, Swift и Kotlin без общего кода.

Потом убедитесь, что в каждой сессии одновременно идёт только одно обновление. Если сразу приходят пять API-запросов, первый запускает обновление, а остальные четыре ждут. Без такой защиты клиенты могут устроить всплеск refresh-запросов, начать гонку и перезаписать хорошие токены старыми.

Понятная последовательность выглядит так:

  1. Проверьте срок действия токена перед защищённым запросом.
  2. Если токен ещё безопасен, отправьте запрос.
  3. Если он уже слишком старый, запустите один refresh-запрос для этой сессии.
  4. Удерживайте остальные защищённые запросы, пока обновление не завершится.
  5. Возобновите ожидающие запросы с новым токеном или завершите сессию, если обновление не удалось.

Правила отказа не менее важны, чем удачный сценарий. Если refresh token отозван, истёк или привязан к устройству, которому сервер больше не доверяет, все клиенты должны выводить пользователя из системы одинаково: очищать сохранённые токены, сбрасывать состояние пользователя и отправлять его на вход. Если обновление не удалось из-за кратковременного падения сети, не считайте это выходом из аккаунта. Покажите ошибку и дайте приложению попробовать снова при следующем запросе.

Небольшой пример делает политику понятнее. Пользователь открывает дашборд в React, когда у токена осталось 20 секунд. Браузер запускает один refresh-запрос. Ещё два запроса за данными аккаунта встают в очередь. На iOS и Android действует то же правило времени, но каждое приложение использует свои инструменты — interceptor, task или обёртку для запроса.

Логируйте одни и те же события авторизации везде. Используйте одинаковые имена событий и причины, например "refresh_started", "refresh_succeeded", "refresh_failed_invalid_token" и "forced_logout_device_untrusted". Когда поддержка видит проблему, эти логи помогают понять, ошибка в одном клиенте или в самом auth-сервисе.

В этом и смысл стратегии обновления токенов. Делитесь правилами, а не кодом.

Задайте один сценарий выхода из аккаунта

Получите помощь fractional CTO
Используйте опытную поддержку CTO, чтобы навести порядок в контракте авторизации, порядке внедрения и распределении ответственности.

Пользователи быстро замечают ошибки выхода. Если web-приложение продолжает показывать старые данные профиля, iPhone очищает всё, а Android ждёт до следующего API-запроса, продукт кажется сломанным, даже если backend работает.

Выберите одну последовательность выхода и используйте её в каждом клиенте. Код не обязан совпадать, но порядок должен быть таким:

  • остановить таймеры обновления, повторы и фоновые запросы
  • очистить токены из памяти
  • удалить сохранённые данные сессии, флаги доверенного устройства и идентификаторы пользователя
  • стереть кэши, зависящие от пользователя
  • перевести приложение на экран, где пользователь не вошёл в систему

Такой порядок предотвращает мелкие утечки. Кэш профиля, открытое соединение или очередь на обновление могут на мгновение показать вышедшего пользователя как вошедшего. Люди это запоминают.

Заранее решите, что делает сервер. Обычный выход из аккаунта обычно должен отзывать текущую сессию устройства или refresh token. Отдельное действие «выйти со всех устройств» должно отзывать каждую активную сессию. Не смешивайте эти действия. Если соединить их, пользователи теряют контроль, а поддержка получает хаос.

Правило одного устройства должно везде оставаться одинаковым. Если человек вышел из аккаунта на телефоне, ноутбук должен остаться в системе, если только он не выбрал глобальный вариант. Команды часто меняют это случайно, потому что каждый клиент делает немного другое предположение.

Принудительный выход должен иметь одну понятную причину, а не расплывчатую ошибку. Показывайте одно сообщение на основе ответа сервера, например: «Вы вышли из системы, потому что пароль был изменён». Это гораздо лучше, чем «Недействительная сессия» или список догадок. Используйте тот же текст причины в React, Swift и Kotlin, чтобы пользователи и поддержка слышали одну и ту же версию.

Простой пример: человек нажимает «Выйти» в приложении Android, оставаясь в системе на web. Android отзывает только сессию этого устройства, очищает локальные данные в том же порядке, что и другие клиенты, и возвращается на экран входа. Приложение React остаётся активным. Если человек выбирает «Выйти везде», каждый клиент должен потерять доступ при следующей проверке сессии.

Обходитесь с доверием к устройству без общего кода

Доверие к устройству должно подчиняться одной политике, даже если каждый клиент хранит данные в своём месте. Сервер решает, что означает слово «доверенное». Каждому приложению нужно хранить только минимум данных, который позволяет спросить: «Это устройство сейчас может пропустить дополнительные проверки?»

Такое разделение упрощает систему. React может опираться на защищённые cookie и небольшой локальный маркер. iOS может использовать Keychain. Android может использовать Keystore и шифрованное хранилище. Эти детали могут отличаться, не меняя набор правил.

Используйте одни и те же состояния доверия во всех клиентах. Достаточно простой модели:

  • Новое устройство: запросить полный вход и дополнительную проверку, например код из email или SMS
  • Доверенное устройство: разрешить обычный вход с сохранённой сессией
  • Чувствительное действие: попросить свежую проверку перед платежами, изменением аккаунта или обновлением пароля
  • Отозванное устройство: принудительно выполнить полный вход и удалить любой локальный маркер доверия

Названия важнее кода. Когда продукт, backend и мобильные команды используют одни и те же состояния, общие сценарии авторизации перестают расходиться.

Задайте чёткие правила для более сильных проверок. Биометрия хорошо работает, когда у пользователя уже есть действующая сессия, но ему нужно открыть чувствительный экран. PIN устройства может быть запасным вариантом на мобильных устройствах. Повторный вход логичнее после смены пароля, восстановления аккаунта, подозрительной смены IP-адреса или долгого бездействия.

В web нужен немного другой подход, потому что браузеры не владеют биометрией так же, как телефоны. Это нормально. Правило может остаться тем же: «Перед чувствительным действием запросите дополнительную авторизацию». На iPhone это может быть Face ID. На Android — отпечаток пальца или PIN устройства. В React — ввод пароля или одноразового кода.

Доверие должно истекать намеренно, а не случайно. Выберите временной интервал и запишите его. Многие команды используют 30 дней для доверенного устройства, а потом сокращают срок для админских действий или финансовых изменений. Если пользователь меняет пароль, снимайте доверие со всех устройств, если только у вас нет ясной причины делать иначе.

Небольшой пример помогает понять логику. Пользователь входит в систему на новом Android-смартфоне и вводит код из email. Сервер отмечает это устройство как доверенное на 30 дней. Через две недели тот же пользователь пытается изменить платёжные данные в приложении React. Сессия всё ещё действительна, но приложение просит свежий пароль или код, потому что действие требует более высокого уровня доверия. Политика одна, поведение клиентов разное.

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

Простой сценарий для React, Swift и Kotlin

Синхронизируйте React, iOS и Android
Согласуйте поведение авторизации в клиентах, не заставляя всех писать общий код.

Миа входит в web-приложение за обедом. Сервер выдаёт ей access token с коротким сроком жизни и refresh token с более долгим сроком, а также те же правила сессии, которые используют все клиенты.

Вечером она открывает приложение на iPhone. Срок access token уже истёк, но в телефоне всё ещё есть действующий refresh token, а устройство по-прежнему подходит под политику доверия. Приложение отправляет один тихий запрос на обновление, получает новые токены и открывает экран аккаунта, не прерывая её.

Для такого потока не нужен общий код. React может использовать обёртку для fetch, Swift — слой на URLSession, а Kotlin — OkHttp authenticator. Важно другое: все три клиента следуют одному контракту — когда access token истёк, сделать один refresh-запрос, сохранить новые токены и затем повторить исходный запрос один раз.

Теперь посмотрим на Android следующим утром. Миа открывает приложение на старом телефоне, которым не пользовалась несколько недель. Приложение пытается сделать тот же шаг обновления, но на этот раз refresh token уже истёк. Сервер возвращает ту же ошибку авторизации, что и в любом другом клиенте. Android очищает локальные данные сессии, прекращает повторы и отправляет её на вход.

Web-приложение должно среагировать так же, если позже попадёт в это состояние. Swift тоже. Дизайн экранов может отличаться, но сообщение — нет. Хорошо работает простая фраза: «Срок действия вашей сессии истёк. Пожалуйста, войдите снова».

Эта мелочь важнее, чем многие команды ожидают. Если web пишет «Unauthorized», iPhone ничего не показывает, а Android пишет «Token error», пользователи думают, что приложения работают по-разному или что-то сломалось. Им не важно, пришла ли проблема из cookie, refresh token или правил доверия к устройству. Им нужен один понятный ответ и один понятный следующий шаг.

Вот как хорошие общие сценарии авторизации выглядят на практике:

  • Web первым выполняет вход и создаёт сессию
  • iPhone делает тихое обновление, потому что сессия всё ещё соответствует политике
  • Android не может обновиться, потому что refresh token больше недействителен
  • Каждый клиент объясняет результат одинаковой пользовательской причиной

Код может оставаться нативным для каждой платформы. Поведение авторизации — нет.

Ошибки, из-за которых авторизация расходится

Общие сценарии авторизации обычно ломаются из-за скучных причин, а не из-за больших ошибок в архитектуре. Одна команда добавляет повтор, другая оставляет локальный флаг, а третья сводит все ошибки авторизации к одному общему сообщению. Через несколько релизов клиенты React, Swift и Kotlin начинают ощущаться по-разному.

Частая ошибка начинается с любого ответа 401. Некоторые приложения считают каждый 401 командой «обновить прямо сейчас». Звучит безопасно, но часто создаёт циклы. Токен может быть истёкшим, отозванным, без нужного права доступа или привязанным к сессии, которую сервер уже закрыл. Если клиент обновляется на каждый 401, приложение может продолжать отправлять тот же запрос, снова терпеть неудачу и загонять пользователя в сломанное состояние вместо того, чтобы вернуть его на вход.

Скрытые повторы создают ещё один хаос. Многие команды забывают, что повторы могут происходить сразу в двух местах: на уровне сети и на уровне авторизации. Тогда один неудачный запрос отправляется дважды. Для запроса профиля это просто неприятно. Для платежа, отправки сообщения или отправки формы это гораздо хуже. Один владелец повторов на клиент — этого достаточно. Все остальные должны не мешать.

Поведение выхода тоже расходится, когда одно приложение очищает меньше состояния, чем другие. Самые очевидные вещи — access и refresh токены. Менее очевидная — доверие к устройству. Если мобильное приложение оставляет маркер «доверенное устройство» после выхода, а web-приложение его удаляет, пользователь получает разное поведение MFA на каждом устройстве. Люди воспринимают это как проблему безопасности, и это справедливо.

Сообщения об ошибках ещё сильнее скрывают рассинхрон. Когда токен не проходит проверку, некоторые клиенты обвиняют сеть. Пользователи видят «проблема с подключением», хотя Wi‑Fi работает нормально, а сессия уже завершена. Это уводит их не туда. Ошибки авторизации должны говорить, что сессия истекла, нужно снова войти в аккаунт или доступ запрещён. Сетевые ошибки оставляйте для настоящих случаев отсутствия сети или таймаута.

Простой набор правил убирает большую часть таких проблем:

  • Обновляйте только для тех ответов сервера, которые в контракте помечены как обновляемые.
  • Повторяйте запрос после обновления только один раз, а не бесконечно.
  • Пусть только один слой управляет повторами для защищённых запросов.
  • Удаляйте маркеры доверия при выходе, если сервер считает выход полным завершением сессии.
  • Разделяйте сообщения для ошибок авторизации и ошибок сети.

В ревью кода эти баги выглядят мелкими. В продакшене они превращаются в двойные действия, случайные выходы из системы и обращения в поддержку, которые трудно воспроизвести.

Быстрая проверка перед релизом

Проверьте правила доверия к устройству
Проверьте срок доверия, дополнительные проверки и поведение при отзыве доступа, прежде чем пользователи увидят случайные запросы.

Мелкие ошибки авторизации редко проявляются в счастливом сценарии. Они всплывают, когда токен истекает во время медленного запроса, когда пользователь выходит из аккаунта на одном устройстве и открывает другое или когда приложение просыпается после нескольких часов в фоне. Поэтому последний проход должен сосредоточиться на нескольких повторяемых проверках, а не только на ручных кликах.

Запустите те же проверки на клиентах React, Swift и Kotlin с одним и тем же тестовым аккаунтом. Код может отличаться. Результат для пользователя — нет.

Чеклист перед релизом

  • Сделайте так, чтобы access token истёк во время API-запроса, и проверьте, что клиент делает одну попытку обновления, а не две или три одновременно. Если несколько запросов падают вместе, приложение должно поставить их в очередь за одним refresh.
  • Отзовите refresh token на сервере и посмотрите, что произойдёт дальше. Каждый клиент должен перестать повторять попытки, очистить состояние авторизации и перевести пользователя на экран, где он не вошёл в систему, в одинаковых ситуациях.
  • Нажмите выход и проверьте локальное состояние. Приложение должно удалить токены, кэшированные данные профиля и любые пользовательские экраны или сохранённые ответы, которые ещё могут появиться после выхода.
  • Запустите сценарий доверенного устройства на каждом клиенте с одним и тем же аккаунтом. Запросы должны появляться в одни и те же моменты, например при первом входе на новом устройстве, после сброса устройства или после истечения записи о доверии.

Одна дополнительная проверка экономит много времени поддержки: войдите на ноутбуке, телефоне и в вкладке браузера, а затем отзовите доступ на сервере. Посмотрите, как быстро каждый клиент заметит изменение. Если одно приложение ждёт до следующей полной перезагрузки, у вас есть проблема рассинхрона, даже если правила API верны.

Команды часто не замечают кэшированные данные. Пользователь выходит из аккаунта, но его имя, переключатель аккаунтов или последний экран ещё на секунду остаются видны. Это выглядит неаккуратно и может раскрыть личные данные на общих устройствах. Проверьте и холодный старт. После выхода снова откройте приложение и убедитесь, что ничего пользовательского не поднимается с диска.

Если вам нужен один простой выпускной критерий, используйте такой: любой тестировщик должен получить одинаковый результат авторизации во всех трёх клиентах в течение минуты, даже если токены истекают, обновление не удаётся или меняется доверие к устройству.

Что делать дальше

Поставьте рядом web-, iOS- и Android-потоки авторизации и найдите расхождения. Не начинайте с кода. Начинайте с поведения. Когда каждое приложение обновляет токен, когда сдаётся, какое сообщение видит пользователь и что именно считается доверенным устройством?

Короткая спецификация обычно исправляет больше, чем ещё одна переработка. Держите её простой и конкретной, чтобы product, backend и client-команды могли использовать её одинаково. Если ваша команда не может ответить на правило одним предложением, значит, оно всё ещё слишком размыто.

Сначала зафиксируйте такие решения:

  • когда обновляются access token и сколько попыток получает каждый клиент
  • какие ответы сервера сразу выводят из системы
  • как помечаются, проверяются и отзываются доверенные устройства
  • что происходит, если приложение не в сети во время обновления
  • что видит пользователь после истечения сессии

Потом проверьте неприятные случаи до того, как менять поведение в продакшене. Истёкшие refresh token важнее счастливого сценария. Как и отозванные сессии, возврат приложения после долгих часов в фоне и пользователи, которые вошли в систему на двух устройствах одновременно. Небольшая таблица тестов ловит рассинхрон авторизации рано и экономит время поддержки позже.

Простой способ внедрения — выбрать один backend-контракт, сначала обновить один клиент и внимательно посмотреть на реальные сессии. После этого подтяните остальные два клиента. Команды часто пытаются обновить React, Swift и Kotlin одновременно. Это звучит аккуратно, но обычно только замедляет отладку.

Если у вас уже есть общие сценарии авторизации, пересматривайте их каждый раз, когда меняете срок сессии, MFA или правила доверия к устройству. Именно такие изменения тихо ломают паритет.

Если перед запуском вам нужен внешний взгляд, Oleg Sotnikov может как fractional CTO проверить контракт, сценарии отказа и порядок внедрения. Этого часто достаточно, чтобы заметить ту самую несостыковку, из-за которой web, iOS и Android расходятся.