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

Почему ловушка токенов — это важно

Современные одностраничные приложения (SPA) используют короткоживущие access-токены и долгоживущий refresh-токен. Когда срок действия access-токена истекает, клиент отправляет запрос на обновление, получает новую пару токенов и повторяет исходный вызов. Большинство разработчиков защищают этот процесс с помощью флага в памяти (например, isRefreshing = true) или очереди запросов, тестируя это в одной вкладке. В реальном мире пользователи держат открытыми несколько вкладок: страницу настроек, панель аналитики и несколько окон с данными. Когда срок действия access-токена истекает, каждая вкладка независимо обнаруживает ошибку 401, каждая отправляет запрос на обновление, и бэкенд — особенно если он применяет ротацию refresh-токенов — воспринимает второй запрос как повтор (replay) и аннулирует всю сессию.

Изоляция JavaScript создает проблему

Каждая вкладка браузера работает в своем собственном контексте JavaScript. Переменные, таймеры и флаги в памяти невидимы для других вкладок, даже если они имеют один и тот же origin. Флаг, сообщающий, что «обновление уже выполняется», существует только внутри той вкладки, которая его установила. Другие вкладки не могут узнать, что токен обновляется где-то еще, поэтому они все запускают собственные сетевые вызовы. Проблема не в баге в интерцепторе; это фундаментальное ограничение изоляции состояния на стороне клиента.

Web Locks API на помощь

Когда вкладка удерживает блокировку, любая другая вкладка, запрашивающая ту же блокировку, должна ждать ее освобождения.

Как это работает для обновления токена

  1. Обнаружение 401 — любая вкладка, получившая ответ «unauthorized», вызывает navigator.locks.request('auth_token_refresh_lock', async lock => { … }).
  2. Получение блокировки — если никакая другая вкладка не удерживает блокировку, текущая вкладка продолжает работу; в противном случае она приостанавливается до освобождения блокировки.
  3. Однократное обновление — владелец блокировки отправляет запрос на обновление, сохраняет новый access-токен и временную метку в localStorage, а затем автоматически освобождает блокировку по завершении callback-функции.
  4. Пропуск дублирующей работы — когда ожидающая вкладка наконец получает блокировку, она считывает временную метку из localStorage. Если токен был обновлен в течение заданного окна (например, за последние несколько секунд), вкладка пропускает сетевой вызов и обновляет свой токен в памяти из localStorage.
  5. Обработка сбоев — если вкладка аварийно завершает работу или закрывается во время удержания блокировки, браузер освобождает ее, позволяя другой вкладке повторить попытку обновления.

Преимущества вкратце

  • Ноль избыточных сетевых вызовов — с бэкендом взаимодействует только первая вкладка.
  • Отсутствие уничтожения сессии — ротация refresh-токенов видит только одно использование, сохраняя сессию активной.
  • Корректное восстановление — управление освобождением блокировок браузером предотвращает взаимные блокировки (deadlocks), если вкладка исчезает.

Чек-лист по реализации

  • Оберните логику обновления в ваш интерцептор Axios (или fetch) с использованием запроса блокировки.
  • Сохраняйте обновленный токен и временную метку в миллисекундах в localStorage (или в sessionStorage, если предпочитаете данные в рамках сессии).
  • При получении блокировки сравните сохраненную метку с Date.now(). Если разница меньше вашего порога, считайте токен из хранилища вместо вызова бэкенда.
  • Убедитесь, что интерцептор обновляет заголовки запросов токеном, полученным из хранилища, перед повторным выполнением исходного API-вызова.
  • Протестируйте процесс с несколькими вкладками, имитируя задержку сети, чтобы убедиться, что до сервера доходит только один запрос на обновление.

Что может пойти не так

На что обратить внимание дальше

Итог

Относитесь к каждой вкладке браузера как к узлу в крошечной распределенной системе. Используя Web Locks API для сериализации обновлений JWT, вы устраняете дублирующие вызовы, защищаете ротацию refresh-токенов и поддерживаете авторизацию пользователя во всех открытых вкладках.