20 сент. 2025 г.·8 мин. чтения

Проблемы с JWT в прототипах: срок действия, ротация, смещение часов

Проблемы с JWT в прототипах часто выглядят случайными. Узнайте, как исправить expiry, ротацию refresh, смещение часов и безопасные схемы хранения токенов.

Проблемы с JWT в прототипах: срок действия, ротация, смещение часов

Почему JWT-токены внезапно перестают работать в прототипах

Большинство проблем с JWT в прототипах проявляются одинаково: вход проходит, а затем пользователи внезапно получают 401, возвращаются на экран входа или видят приложение, которое работает только после обновления страницы.

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

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

  • Время: expiry (exp), issued-at (iat), смещение часов, короткоживущие access-токены
  • Хранение: токен отсутствует, перезаписан, очищен или застрял в неправильном месте
  • Валидация: неверный секрет/публичный ключ, неправильные aud/iss, токен отозван или ротирован

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

  • Точный ответ с ошибкой (статус + сообщение) и на каком endpoint это произошло
  • Отметку времени с клиента и из логов сервера для того же запроса
  • Время устройства и часовой пояс (особенно на мобильных)
  • Полезные поля токена (claims): exp, iat, iss, aud и время выпуска

Конкретный пример: основатель тестирует на Mac — всё работает. Пользователь на Windows постоянно выскакивает из аккаунта каждые 10–15 минут. Реальная причина не «случайные логауты» — системное время пользователя отстаёт на 6 минут, access-токен короткий, и сервер отказывает без допусков. Добавьте вторую вкладку — и одна вкладка может обновлять токены, а другая перезаписывать хранилище.

Если ваш прототип сгенерирован инструментами вроде Lovable, Bolt или Cursor, часто встречаются расхождения в обработке токенов между страницами. FixMyMess нередко находит смесь коротких сроков, отсутствующей логики обновления и небезопасного хранения одновременно.

Основы JWT, которые нужны для отладки (без теоретической лапши)

JWT — это просто подписанная записка. Сервер подписывает её, чтобы позже можно было проверить, что токен выпущен вами и не был изменён. Большинство JWT не шифруются, поэтому любой, у кого есть токен, может декодировать и прочитать содержимое.

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

Пара полей, которые объясняют большинство сбоев

Когда токены «внезапно перестают работать», обычно виноваты эти поля или то, как приложение их проверяет:

  • exp (expires at): момент, после которого токен надо отвергнуть.
  • iat (issued at): когда токен был создан. Часто помогает отлаживать проблемы со временем.
  • nbf (not before): токен должен быть отклонён до этого времени.
  • aud (audience): для кого предназначен токен (конкретное API).
  • iss (issuer): какая система выпустила токен.
  • sub (subject): пользователь или сущность, которую представляет токен.

Типичный несоответствующий прототип: фронтенд шлёт токен в API B, а токен выпущен для API A (неправильный aud), или бэкенд в одном окружении строг по iss, а в другом — нет.

«Не работает» может означать два противоположных состояния

Проблемы с JWT часто возникают либо из‑за отсутствия проверок, либо из‑за чрезмерно строгих проверок.

Если проверок нет, токены могут работать тогда, когда не должны (дырa в безопасности), а потом «внезапно ломаются», когда вы добавляете правило валидации.

Если проверки слишком строгие, пользователей выкидывает из системы из‑за мелких различий — например, серверные часы на 60 секунд вперёд (об этом ниже) или токен, который валиден, но не совпадает с точной строкой aud/iss.

Что никогда не должно быть в JWT

Не кладите в payload секреты или чувствительные данные: пароли, API‑ключи, учётные данные базы данных, коды сброса или персональные данные, которые вы не хотите видеть в таблице. Держите минимал: sub (id), возможно роль или пара прав, и временные поля.

Если вы наследуете AI‑сгенерированный прототип, это частая проблема: токены содержат внутренние ключи или слишком много пользовательских данных и затем хранятся небезопасно. Даже если аутентификация «работает», это один шаг до серьёзного инцидента.

Проблемы с истечением: exp и iat, которые вызывают неожиданные разлогинивания

Большинство проблем с JWT в прототипах не «случайны». Обычно это арифметика истечения, которая выглядит нормально в локальном тестировании, но ломается при реальных устройствах, сетях и отличиях во времени.

Пару ошибок вызывают большинство неожиданных разлогиниваний:

  • exp слишком маленький. Пять минут звучит безопасно, но это тяжело для прототипов с долгим холодным стартом, фоновыми мобильными приложениями и нестабильным подключением. Пользователь возвращается через короткий перерыв, и все вызовы падают.
  • exp отсутствует. Некоторые библиотеки позволят токены без expiry, некоторые — нет, и ваш собственный код может трактовать их как просроченные. Это создаёт путаное поведение в разных окружениях.
  • iat в будущем. Такое случается, когда серверные часы неверны, используется неправильный часовой пояс или токены создаются в одном сервисе, а проверяются в другом. Многие валидаторы отвергают токены «ещё не действительные».

Практический шаблон по умолчанию: короткоживущий access-токен + долгоживущий refresh-токен. Делайте access‑токен достаточно коротким, чтобы ограничить ущерб при компрометации, но достаточно длинным, чтобы выдержать обычное использование. Для многих прототипов хороший старт — 10–20 минут для access и дни для refresh. Позже можно ужесточить.

Поведение клиента так же важно, как и настройки токенов. Хорошее правило: обрабатывайте 401 один раз.

  1. Если запрос вернул 401, вызовите refresh.
  2. Если refresh успешен, повторите исходный запрос один раз.
  3. Если повтор или refresh неудачны, разлогиньте и покажите понятное сообщение.

Это избегает бесконечных циклов повторов и экранов, которые «крутятся вечно».

На сервере избегайте неясных 500 при просрочке токена. Возвращайте чёткий 401 для просроченных или недействительных access‑токенов и явный 401 (или 403, если предпочитаете), когда refresh‑токен недействителен или отозван. Это показывает, есть ли проблема с истечением, или это реальный сбой бэкенда.

Пример: основатель тестирует на ноутбуке — всё работает. Пользователи на мобильных открывают приложение после обеда, access‑токен просрочен, и приложение продолжает слать API старый токен, пока не сдастся. С паттерном «один 401 → обновление → повтор» такие пользователи продолжат работу без заметных сбоев.

Если вы унаследовали AI‑сгенерированный flow, где токены «иногда» падают, FixMyMess часто находит комбинацию слишком малого exp, непоследовательной валидации и отсутствующей логики обновления.

Смещение часов: когда корректные токены падают из‑за неверного времени

Некоторые проблемы с JWT не вызваны плохим кодом или «недействительными» токенами. Токен может быть корректен, подписан и неизменён, но всё равно провалиться, потому что часы устройства или сервера неверны.

Смещение часов встречается чаще в прототипах, чем думают: телефон с вручную выставленным временем, VM с дрейфом, контейнер с неправильно настроенным источником времени или два сервера в одном приложении, чьи часы расходятся на минуту.

Когда время неверно, вы обычно видите отказы вокруг временных полей:

  • nbf: сервер считает, что токен ещё не активен
  • exp: сервер считает, что токен уже просрочен
  • iat: некоторые библиотеки используют его для дополнительных проверок и могут отвергнуть «токены из будущего»

Обычный сценарий: вы подписали токен на Сервере A, но запрос пользователя валидируется на Сервере B, часы которого опережают на 45 секунд. Вдруг пользователи «случайно» выходят из системы, или вход работает, а на следующей странице — нет.

Практическое решение: небольшой leeway при валидации

Большинство JWT‑библиотек позволяют добавить небольшую толерантность при проверке exp и nbf. Хорошая отправная точка — 30–120 секунд. Держите допуск маленьким: он должен покрывать дрейф, а не продлевать сессии.

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

Операционное решение: сделать время одинаковым везде

Leeway уменьшает ложные отказы, но корень проблемы всё равно надо исправлять. Быстрые проверки, которые ловят большинство проблем:

  • Убедитесь, что все серверы и билд‑агенты синхронизируются с одним источником времени (NTP)
  • Не смешивайте хосты с корректным временем и контейнеры с изолированным или неправильно настроенным временем
  • Проверьте, что мобильные тестовые устройства установлены на автоматическое время
  • В мультисерверной конфигурации убедитесь, что запросы не прыгают между нодами с разным временем

Если ваш прототип сгенерирован AI‑инструментом (часто Lovable, Bolt, v0, Cursor или Replit), баги со смещением часов могут маскироваться под «у меня всё работает на моей машине». При аудите FixMyMess мы часто видим, что небольшая толерантность плюс правильная синхронизация времени устраняют флаттерящие логауты без полной переработки аутентификации.

Refresh‑токены: недостающая часть в большинстве прототипов

Превратите 401 в понятные исправления
Трассируем падающие запросы до точной причины: время, хранение или валидация.

Большинство проблем с JWT в прототипах происходят потому, что приложение пытается использовать один токен для всего: короткоживущий access‑токен должен ещё держать пользователя в системе днями. Это и создаёт ощущение случайности. Refresh‑токен нужен, чтобы поддерживать сессию без удлинения срока жизни access‑токена (что рискованно при утечке).

Access‑токены должны быть обычными: короткий срок, часто посылаются, легко заменяются. Refresh‑токены — редкие: используются только для получения нового access‑токена и храниться в местах, недоступных JavaScript и логам.

Типичная ошибка прототипа — хранить refresh‑токен так же, как access‑токен (например, в localStorage) и обращаться с ним как с обычными учётными данными API. При утечке такого токена злоумышленник сможет выпускать новые access‑токены, пока вы это не заметите. Другая ошибка — вообще отсутствующий refresh‑токен, и «решение» — задать access‑токену гигантский срок жизни. Это превращается в долговой риск безопасности позже.

Перед тем как строить flow, решите, что значит «сессия» для вашего продукта:

  • Как долго пользователь должен оставаться залогиненным без открытия приложения?
  • Закрытие браузера должно разлогинивать или нет?
  • Хотите ли вы одну сессию на устройство, или вход на одном устройстве должен вытеснять другой?
  • Что происходит после смены пароля: разлогинить везде или только в новых сессиях?

Реальная эксплуатация добавляет краевые случаи, которые прототипы редко покрывают. Пользователи открывают несколько вкладок, сеть может прерваться в середине запроса, и два запроса одновременно могут попытаться обновить токен. Если вы это не контролируете, получаете циклы (обновление, ошибка, повтор) или внезапные 401, которые выглядят как «проблемы с JWT», даже когда токены в порядке.

Если вы унаследовали AI‑сгенерированный прототип (Lovable, Bolt, v0, Cursor, Replit), слой обновления часто отсутствует или наполовину реализован. Исправление этого обычно первым снимает баги «вчера всё работало».

Пошагово: рабочий flow обновления с ротацией

Большинство ошибок проявляются, когда access‑токены истекают, а приложение не умеет надёжно восстанавливаться. Flow с ротацией refresh‑токенов делает истечение нормальным, а не пугающим.

Flow (от входа до обновления и повторной попытки)

Начинайте с генерации двух токенов при входе: короткоживущий access‑токен (минуты) и долгоживущий refresh‑токен (дни или недели). Access‑токен проверяется на каждом API‑вызове. Refresh‑токен используется только для получения нового access‑токена.

Простой надёжный сценарий:

  • При входе возвращается access‑токен + refresh‑токен
  • Клиент вызывает API с access‑токеном, пока не получит 401
  • Клиент вызывает endpoint обновления с refresh‑токеном
  • Сервер возвращает новый access‑токен и новый refresh‑токен
  • Клиент повторяет исходный API‑запрос один раз

Ротация — ключевая деталь: каждое обновление выдаёт совершенно новый refresh‑токен, а старый становится недействительным. Тогда, если refresh‑токен утёк, он перестаёт работать, как только реальный пользователь обновит токен.

Правила ротации, которые нужно реализовать на сервере

Чтобы ротация была безопасной (и не ломала пользователей), соблюдайте строгие правила:

  • Храните refresh‑токены на сервере (хешированные) с id пользователя, сроком и уникальным id токена.
  • При обновлении принимайте только текущий token id, сразу помечайте его использованным/отозванным и выдавайте новый token id.
  • Если старый токен предъявляется снова, считайте это подозрительным и ревокуйте всю сессию (или требуйте повторный вход).

Конкурентность — место, где прототипы часто ведут себя нестабильно. Две вкладки, двойной клик или повтор приложения могут триггерить два обновления одновременно. Небольшая стратегия мягкости предотвращает неожиданные логауты: позвольте только что ротированному refresh‑токену быть использованным ещё один раз в коротком окне (например 10–30 секунд), но только если вы можете определить, что он принадлежит одной и той же цепочке сессии.

Если вы боретесь с «случайными» проблемами JWT, чаще всего причина в том, что ротация, хранение или конкурентность реализованы наполовину. Команды часто приносят FixMyMess прототипы из Lovable/Bolt/v0/Cursor/Replit, где refresh кажется «сделанным», но ломается под реальной нагрузкой — мы быстро делаем его продакшен‑безопасным.

Безопасные схемы хранения токенов (веб и мобайл)

Исправить ротацию refresh-токенов
Укрепляем flow обновления, чтобы пользователи оставались в системе без рискованных долгоживущих access-токенов.

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

Веб: относитесь к refresh‑токену как к паролю

Для браузерных приложений безопаснее по умолчанию хранить refresh‑токен в httpOnly, Secure cookie. httpOnly предотвращает доступ JavaScript (это сильно помогает при XSS). Secure гарантирует передачу только по HTTPS.

Флаги cookie — не «установил и забыл». Делайте их осознанным выбором:

  • httpOnly + Secure: базовый хороший выбор для refresh‑токенов.
  • SameSite=Lax: обычно подходит для обычной навигации и снижает риск CSRF.
  • SameSite=None: нужен для кросс‑сайтовых сценариев (разные домены), но требует Secure и увеличивает риск CSRF.
  • Защита от CSRF: если endpoint обновления основан на cookie, добавьте защиты CSRF (например, CSRF‑токен в заголовке или схема двойной отправки).

Избегайте хранения долгоживущих токенов в localStorage или sessionStorage. Это удобно, но если на странице выполнится любой скрипт (XSS, скомпрометированная зависимость, расширение браузера), он сможет прочитать токены и вывести их.

Access‑токены — другое дело: держите их короткоживущими и по возможности в памяти. Если обновление страницы их теряет, это нормально: refresh‑cookie может выдать новый.

Мобильные приложения: используйте безопасное хранилище, держите access‑токены короткими

На iOS и Android храните refresh‑токены в безопасном хранилище платформы (Keychain на iOS, Keystore на Android). Не кладите refresh‑токены в обычное хранилище приложения или в логи.

Практическая схема:

  • Refresh‑токен: защищённое хранилище, долгий срок, ротируется.
  • Access‑токен: только в памяти, короткий срок, часто обновляется.
  • При фоновом режиме/закрытии приложения: сбрасывайте access‑токен и получайте новый при возобновлении.

Ещё один тихий источник «случайных» отказов: токены попадают туда, куда вы не ожидаете. Никогда не помещайте access‑токены в URL (query params), очищайте их из логов, аналитики, отчётов об ошибках и всплывашек. Например, если приложение логирует весь запрос при падении API‑вызова, оно может случайно захватить заголовок Authorization.

Если вы унаследовали AI‑сгенерированный flow, который смешивает cookie, localStorage и долгоживущие access‑токены, FixMyMess быстро проведёт аудит и укажет точные места утечек и небезопасные решения перед релизом.

Типичные ловушки прототипов, которые делают аутентификацию нестабильной

Баги JWT в прототипах часто кажутся случайными, потому что система наполовину строгая и наполовину либеральная. Она «работает на вашей машине», а затем ломается после деплоя, добавления сервиса или открытия пользователем второй вкладки.

Ловушка 1: пропуск проверки issuer (iss) и audience (aud)

Многие прототипы проверяют только подпись и срок действия. Это может быть достаточно для одного бэкенда, но становится хрупким, как только вы добавите второй API, фонового воркера или отдельный админ‑сервис.

Если вы не проверяете iss и aud, можно принять токен, предназначенный для другой системы, а потом «ужесточить безопасность» и внезапно получить 401 у реальных пользователей, потому что существующие токены не соответствуют новым правилам.

Проще избежать сюрпризов так:

  • Один строковый iss для вашего auth‑сервиса
  • По одному aud на API (или один общий, если у вас действительно один API)
  • Согласованные настройки в dev, staging и prod

Ловушка 2: секреты для подписи меняются между окружениями или деплоями

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

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

Ловушка 3: доверие клиентскому декодированию вместо проверки на сервере

Декодирование JWT в браузере (или приложении) не равно его верификации. Прототип может читать payload и полагать, что пользователь залогинен, откладывая реальную server‑проверку. Это создаёт несогласованные состояния: UI показывает «вошёл», а API отклоняет запросы.

Сделайте сервер источником истины: клиент должен воспринимать 401 от API как реальный сигнал на обновление или повторный вход.

Ловушка 4: неправильный кэш состояния аутентификации (особенно между вкладками)

Распространённый баг: одна вкладка обновляет токены, другая продолжает шлать старый access‑токен, и приложение «прыгает» между рабочим и нерабочим состоянием. Ещё вариант: вы кэшируете «текущего пользователя» в памяти и никогда не обновляете после refresh.

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

Ловушка 5: отладочные хелперы аутентификации в проде

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

Если вы унаследовали AI‑сгенерированный прототип и аутентификация хрупкая, FixMyMess может быстро провести аудит кода (включая проверку токенов, логику ротации и схемы хранения) и указать на конкретные ловушки до того, как пользователи наткнутся на них.

Пример: у вас работает, а у пользователей постоянно выход из системы

Исправим за 48–72 часа
Большинство проектов завершаются за 48–72 часа с 99% успеха.

Типичная история: вы тестируете приложение весь день на своём ноутбуке — аутентификация кажется стабильной. Вы открываете превью для реальных пользователей, и начинаются жалобы: «меня выкидывает каждые 10 минут» или «вход работает один раз, а потом случайно перестаёт».

Реалистичный сценарий. Основатель собирает прототип в AI‑инструменте, запускает локально и входит в том же браузере. В проде пользователи открывают приложение на телефоне, меняют сеть, убирают приложение в фон, а потом возвращаются. Именно здесь проявляются проблемы JWT: короткие сроки жизни, разница в часах и логика обновления, рассчитанная только на счастливый путь.

Как это воспроизвести (без догадок)

Преобразуйте «случайные логауты» во временную шкалу. Попросите одного пострадавшего пользователя сказать, когда он вошёл и когда вышел (важен его часовой пояс). Затем соберите один падающий запрос на сервере (или в логах) и зафиксируйте:

  • Время сервера, когда запрос был отклонён (401)
  • Значения exp и iat токена (декодированные на сервере)
  • Был ли попытка refresh и упала ли она
  • Был ли у пользователя несколько устройств или вкладок

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

Наиболее вероятные причины

В прототипах эти ошибки повторяются:

  • exp слишком короткий (5–15 минут) и нет надёжного flow обновления — сюрприз‑логауты.
  • Смещение часов: бэкенд проверяет exp и iat без допусков, а одна машина имеет неверное время.
  • Баги ротации refresh‑токенов: вы ротируете, но не обрабатываете повторное использование токена или перезаписываете сохранённый refresh‑токен при гонке двух запросов.

Практический путь исправления

Исправления обычно просты, если действовать по порядку.

Сначала добавьте небольшой leeway при валидации временных полей (часто 30–120 секунд). Это останавливает ложные просрочки от смещения часов.

Далее сделайте обновление надёжным. Когда access‑токен истёк, выполните одно обновление и повторите запрос. Если несколько запросов одновременно сталкиваются с истечением, гарантируйте, что обновление выполнит только один запрос, а остальные дождутся его результата.

Наконец, укрепите хранение. Используйте защищённые cookie для веба (httpOnly, Secure, правильно выставленные SameSite) и избегайте долгоживущих refresh‑токенов в localStorage. На мобайле — платформа‑безопасное хранилище.

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

Короткий чек‑лист и следующие шаги

Когда проблемы с JWT кажутся «случайными», обычно это не так. Токен падает по ограниченному набору причин: неверные числа внутри, часы сервера врозь, неполный flow обновления или уязвимое хранение.

Начните с быстрого доказательства, а не с догадок. Скопируйте падающий токен, декодируйте и запишите exp, iat, iss, aud и sub. Затем сравните это с тем, что ваш API реально проверяет в этом окружении.

Ниже короткий чек‑лист, который ловит большинство проблем прототипов:

  • Декодируйте падающий токен и убедитесь, что exp в будущем, а iat не «в будущем» относительно времени API сервера.
  • Проверьте время API: серверные часы, время контейнера и синхронизацию хостинга. Даже несколько минут дрейфа ломают строгую валидацию.
  • Подтвердите конфигурацию подписи: секрет/приватный ключ корректен для этого окружения (dev vs preview vs prod) и вы не смешиваете ключи между сервисами.
  • Валидируйте aud/iss: они должны совпадать с ожиданиями API во всех окружениях (особенно в preview‑деплойах).
  • Протестируйте поведение обновления: endpoint refresh надёжно возвращает новый access‑токен, а ротация refresh‑токенов не инвалидирует пользователей с активной сессией.

После этого сделайте один end‑to‑end тест, как реальный пользователь: войдите, дождитесь истечения access‑токена, затем вызовите API и посмотрите, как приложение обновляет и повторяет запрос. Если это не работает, ищите: refresh‑токен не отправлен (неправильные флаги cookie), refresh‑токен перезаписан при ротации или клиент вообще не повторяет исходный запрос.

Проверка хранения — последний быстрый шаг: не класть refresh‑токены в localStorage. Для веб‑приложений httpOnly cookie обычно безопаснее по умолчанию. Для мобильных — используйте защищённое хранилище платформы.

Если ваш AI‑сгенерированный прототип (Lovable, Bolt, v0, Cursor, Replit) имеет нестабильную аутентификацию и вам нужно быстро подготовить его к продакшену, FixMyMess может выполнить бесплатный аудит кода, точно указать, где ломается логика обновления, ротация или хранение, и безопасно это исправить.