Короткий ответ
Access-токен делают короткоживущим (минуты), а refresh-токен — долгоживущим, но одноразовым: каждое его использование для получения нового access-токена сразу инвалидирует старый refresh-токен и выдаёт новый (ротация), а все refresh-токены хранятся на сервере с возможностью немедленного отзыва.
Как это работает подробнее
Сервер хранит refresh-токены (или их хеши) в БД с привязкой к пользователю и устройству/сессии; при каждом обмене refresh-токена на новый access-токен старый refresh-токен помечается использованным, и если он вдруг будет предъявлен повторно — это сигнал компрометации, и можно отозвать всю цепочку токенов пользователя.
Пример кода
example.txt
POST /auth/refresh {"refresh_token": "rt_old"}
-> проверить rt_old в БД: не использован, не истёк
-> пометить rt_old как использованный
-> выдать rt_new, access_new
-> сохранить rt_new в БД
POST /auth/refresh {"refresh_token": "rt_old"} (повторное предъявление уже использованного)
-> обнаружен reuse -> отозвать все активные refresh-токены пользователя, потребовать повторный логинЧто использовать на практике
- Практика. Короткий срок жизни access-токена ограничивает окно атаки при его утечке (например, через XSS), а серверная инвалидация refresh-токена при logout/подозрительной активности невозможна для чисто stateless JWT без дополнительного состояния — поэтому refresh-токены обычно делают stateful, даже если access-токен остаётся JWT.
Подводные камни
- Риск. Делают оба токена с одинаково долгим сроком жизни «для удобства пользователя»; не реализуют ротацию, из-за чего украденный один раз токен работает сколь угодно долго; хранят refresh-токен в localStorage, уязвимом к XSS, вместо httpOnly cookie.