JWT в DRF: чек-лист перед продакшеном
JWT в Django REST Framework часто настраивают за пять минут по документации и выкатывают. А потом токены не отзываются, лежат в localStorage и живут по неделе. Разбираем, что проверить до релиза.

Типичная история: команда ставит djangorestframework-simplejwt, копирует конфиг из README, получает пару access/refresh и радуется. Через месяц выясняется, что разлогинить пользователя невозможно, скомпрометированный refresh живёт неделю, а фронтенд хранит токены в localStorage, откуда их вытащит любой XSS. Ни одна из этих проблем не всплывает в тестах — только в проде.
Этот материал — не введение в JWT и не пересказ документации simplejwt. Здесь только то, что стоит проверить перед выкатыванием аутентификации по токенам в боевую среду. Ориентируюсь на DRF 3.15.x и djangorestframework-simplejwt 5.3.x. Если вы используете DEFAULT-конфиг библиотеки как есть — почти все пункты ниже вас касаются.
Сначала честно: нужен ли вам JWT
Самая частая ошибка — брать JWT туда, где хватает TokenAuthentication из коробки DRF. Разница в цене.
| Критерий | DRF TokenAuthentication | SimpleJWT |
|---|---|---|
| Хранение | Токен в БД, одна строка на пользователя | Токен не хранится, подпись проверяется по ключу |
| Отзыв (logout) | Удалить строку — токен мёртв мгновенно | Access отозвать нельзя до истечения; refresh — только через blacklist |
| Масштабирование | Запрос в БД на каждый вызов | Проверка подписи без БД |
| Срок жизни | Бессрочный по умолчанию | Access короткий, refresh длинный |
Если у вас монолит на одном сервере и не сотни тысяч RPS — TokenAuthentication закрывает потребности и проще в отзыве. JWT оправдан, когда несколько сервисов проверяют токен без общей БД, либо когда критичен короткий срок жизни access-токена. Дальше — про случай, когда вы уже выбрали SimpleJWT.
Чек-лист перед продом
1. Access-токен должен быть коротким
По умолчанию ACCESS_TOKEN_LIFETIME в simplejwt — 5 минут, REFRESH_TOKEN_LIFETIME — 1 день. Это разумная база, но многие увеличивают access до часа «чтобы не дёргать refresh». Так делать не стоит: access отозвать нельзя, поэтому его срок жизни — это окно, в течение которого украденный токен работает.
# settings.py, djangorestframework-simplejwt 5.3.x
from datetime import timedelta
SIMPLE_JWT = {
"ACCESS_TOKEN_LIFETIME": timedelta(minutes=5),
"REFRESH_TOKEN_LIFETIME": timedelta(days=1),
# ротация refresh при каждом обновлении
"ROTATE_REFRESH_TOKENS": True,
# старый refresh в чёрный список после ротации
"BLACKLIST_AFTER_ROTATION": True,
}Проверка: запросите access, подождите его срок жизни, повторите запрос к защищённому эндпоинту — должен вернуться 401 с кодом token_not_valid.
2. Отзыв токенов должен работать
Без blacklist-приложения logout — это иллюзия. Пользователь «вышел», а его refresh продолжает генерировать новые access-токены до конца своего срока жизни. Включите blacklist явно:
- добавьте
rest_framework_simplejwt.token_blacklistвINSTALLED_APPS; - выполните
python manage.py migrate— появятся таблицыOutstandingTokenиBlacklistedToken; - включите
ROTATE_REFRESH_TOKENSиBLACKLIST_AFTER_ROTATION, как выше; - для logout принимайте refresh-токен и вызывайте у него
.blacklist().
# views.py — эндпоинт logout, simplejwt 5.3.x
from rest_framework.views import APIView
from rest_framework.response import Response
from rest_framework import status
from rest_framework.permissions import IsAuthenticated
from rest_framework_simplejwt.tokens import RefreshToken
from rest_framework_simplejwt.exceptions import TokenError
class LogoutView(APIView):
permission_classes = [IsAuthenticated]
def post(self, request):
refresh = request.data.get("refresh")
if not refresh:
return Response(
{"detail": "refresh обязателен"},
status=status.HTTP_400_BAD_REQUEST,
)
try:
token = RefreshToken(refresh)
token.blacklist() # добавляет refresh в BlacklistedToken
except TokenError:
return Response(status=status.HTTP_400_BAD_REQUEST)
return Response(status=status.HTTP_205_RESET_CONTENT)Важно понимать границу: blacklist работает только для refresh-токенов. Access-токен по-прежнему проживёт до истечения своего срока. Именно поэтому пункт 1 не опционален — короткий access и есть ваш реальный механизм «отзыва».
JWT без blacklist и с длинным access-токеном — это не аутентификация с выходом из системы, а пропуск, который нельзя аннулировать. Пользователь нажимает «Выйти», а сервер об этом не знает.
3. Где хранит токены фронтенд
Это не про Django, но именно здесь ломается безопасность чаще всего. localStorage доступен любому JavaScript на странице — при XSS токен утекает. Варианты по убыванию риска:
| Хранение | Защита от XSS | Защита от CSRF | Цена |
|---|---|---|---|
| localStorage | Нет | Да (токен в заголовке) | Просто, но опасно |
| httpOnly Secure cookie для refresh | Да | Нужен CSRF-механизм | Сложнее, безопаснее |
Рабочая схема для веба: refresh — в httpOnly, Secure, SameSite=Strict куке, access — в памяти вкладки (переменная, не хранилище). При обновлении вкладки access теряется и восстанавливается через refresh из куки. Для мобильного приложения куки не нужны — используйте безопасное хранилище платформы.
4. SECRET_KEY и алгоритм подписи
По умолчанию simplejwt подписывает токены алгоритмом HS256 вашим Django SECRET_KEY. Проверьте два момента:
SECRET_KEYне лежит в репозитории и не совпадает с dev-ключом. Токены, выпущенные dev-ключом, будут валидны в проде, если ключ утёк;- если токен проверяют несколько сервисов, а выпускает один — переходите на RS256 (асимметричная подпись). Тогда приватный ключ есть только у сервиса-эмитента, остальные проверяют публичным. Настраивается через
ALGORITHM,SIGNING_KEYиVERIFYING_KEYвSIMPLE_JWT— конкретные имена и формат смотрите в разделе Settings документации simplejwt для вашей версии.
Смена SECRET_KEY в проде разом инвалидирует все выпущенные токены — учитывайте это при ротации ключа.
5. HTTPS и передача токена
Токен в заголовке Authorization: Bearer ... по HTTP читается кем угодно на пути пакета. В проде — только HTTPS, а на уровне Django включите SECURE_SSL_REDIRECT = True и, если refresh в куке, SESSION_COOKIE_SECURE и CSRF_COOKIE_SECURE. Кука с токеном обязательно с флагом Secure, иначе браузер отправит её и по HTTP.
6. Что кладёте в payload
Payload JWT закодирован base64, а не зашифрован — его читает любой, кто держит токен в руках. Не кладите туда телефон, email, роли с чувствительным смыслом. По умолчанию simplejwt пишет user_id, exp, iat, jti и тип токена — этого достаточно. Расширяете payload через свой сериализатор для получения токена — но каждое добавленное поле проверяйте на «а не палю ли я лишнее».
Если не сработало
- 401
token_not_valid, «token is invalid or expired» — access истёк. Клиент должен обновить его через refresh-эндпоинт, а не логинить пользователя заново. Проверьте, что фронтенд ловит 401 и делает refresh. - logout возвращает 400, blacklist не срабатывает — не выполнена миграция
token_blacklistили приложение не добавлено вINSTALLED_APPS. Проверьте наличие таблицыtoken_blacklist_blacklistedtokenв БД. - Токен считается валидным после logout — вы отозвали refresh, но проверяете access. Это ожидаемо: access живёт до
exp. СократитеACCESS_TOKEN_LIFETIME. - После смены SECRET_KEY всех разлогинило — это не баг. Подпись старым ключом больше не проходит. Планируйте ротацию ключа как событие с массовым релогином.
- «Authentication credentials were not provided» — заголовок называется не так или префикс не
Bearer. По умолчанию simplejwt ждётAUTH_HEADER_TYPES = ("Bearer",). Проверьте, что клиент шлёт именно его.
Что мерить и делать дальше
После выката смотрите на долю 401 с token_not_valid в метриках: резкий рост — признак, что клиент не обновляет access через refresh и дёргает пользователя. Настройте алерт на аномальный рост записей в OutstandingToken — это косвенный индикатор перевыпуска токенов или атаки перебором логина.
Следующий шаг — периодическая очистка таблицы OutstandingToken от истёкших записей: она растёт при включённой ротации. В simplejwt есть management-команда для очистки просроченных токенов — точное имя команды сверьте в документации вашей версии, оно менялось между релизами. Инвалидация access-токенов до истечения (например, при бане пользователя) — отдельная тема со своим механизмом на стороне сервиса, здесь мы её не разворачивали.
Тендерный специалист 44-ФЗ / 223-ФЗ: Разбор PDF-документации закупки, поиск «заточек» под поставщика, чек-лист заявки.
Спросить за 20 ₽Источники: djangorestframework-simplejwt — официальная документация, Django REST Framework — Authentication
Частые вопросы
У меня simplejwt другой версии, конфиг тот же?
Можно ли отозвать access-токен сразу, не дожидаясь истечения?
А если я не успею внедрить blacklist до релиза — это критично?
Стоит ли хранить токены в localStorage, если у меня SPA?
Нужен ли мне JWT вообще или хватит TokenAuthentication из DRF?
Почему после logout старый access-токен всё ещё работает?
Материал носит информационный характер и подготовлен редакцией «Агентуры». Он не является офертой, рекламой или индивидуальной консультацией. Упомянутые продукты, компании и торговые знаки принадлежат их правообладателям. Перед принятием решений, влекущих юридические или финансовые последствия, обратитесь к профильному специалисту.