## 1. «Я вошёл в систему, а меня не узнают?». Как всё началось {#sec-ffb74987f051} OAuth2, JWT, сессионная аутентификация… Способов аутентификации множество, и в большинстве случаев их достаточно. Я тоже так думал. - Когда я добавлял **OAuth2** в собственный почтовый клиент или в MyGPT от ChatGPT, было ощущение «вот это пользовательский опыт»; - Для одностраничных Django‑приложений лучшим вариантом оказалась **сессионная аутентификация**; - При разделённой архитектуре фронтенд/бэкенд самым чистым решением выглядел **JWT**. Однако одна комбинация однажды полностью сломалась – **асинхронные задачи (Celery)**. Пользователь нажимает кнопку, а вместо того, чтобы бекенд сразу обработал запрос, работа передаётся удалённому AI‑серверу или воркеру. И воркер спрашивает: > «Эй… запрос получен, но для кого? request.user у меня нет». ![Робот‑воркер с API‑ключом передаёт письмо](/media/editor_temp/6/b63803ff-21ba-4f1b-a5af-47c8ca0fdd25.png) ## 2. Проблема «бэкенд ↔ бэкенд» + «асинхронный воркер (Celery)» {#sec-a7eab2678145} Ключевой момент, заставивший меня внедрить API‑ключ, был в том, что **между бекенд‑серверами** появляется **Celery‑воркер**. 1. Пользователь отправляет HTTP‑запрос; 2. Бэкенд кладёт задачу в очередь; 3. Celery‑воркер берёт задачу и отправляет асинхронный запрос на какой‑то вычислительный сервер. Самая боль в этой цепочке: - У воркера **нет request.user**; - Нет сессии (это не браузер); - JWT сложно использовать – непонятно, кто, где и как будет хранить и передавать токен; - OAuth2 требует пользовательского взаимодействия и просто не подходит. Когда JWT и сессии теряют силу, остаётся один вопрос: > «Как воркеру указать, от имени какого клиента (тенанта/пользователя) выполнять задачу?» ## 3. Для воркера важнее «идентификация», чем «аутентификация» {#sec-77f5863a0a93} В веб‑запросе «аутентификация = вход», а «вход = пользователь». Воркер же – это не человек, а приложение, которое самостоятельно «берёт» процессор. Поэтому сначала нужна **идентификация**, а аутентификацию можно решить простыми HMAC или secret‑key. - Задача должна выполняться с данными клиента **A**; - Результат должен сохраняться в ресурсах клиента **A**; - Тариф, права и квоты учитываются по клиенту **A**. Пытаться впихнуть JWT или сессию в эту схему приводит к росту сложности: генерация, хранение, передача, истечение, обновление токенов. И возникает непреодолимое чувство: «Какой смысл выдавать JWT серверу, если пользователь не использует браузер прямо сейчас?» – идея была быстро отвергнута. ## 4. Решение: API‑ключ оказался простым и мощным {#sec-2110738d8481} Мы внедрили **API‑ключ** и сразу решили проблему. - Воркер при внутреннем запросе передаёт **один заголовок** для аутентификации и идентификации; - Сервер по ключу мгновенно определяет, чей это запрос; - Обновление/замена ключа (ротация) становится предельно прозрачной. Пример запроса к вычислительному серверу: ```http POST /v1/ai/jobs Authorization: Api-Key Content-Type: application/json { "job_id": "...", "payload": {...} } ``` Воркеру не нужен `request.user` – достаточно отправить запрос с API‑ключом, а принимающий бэкенд определит пользователя по этому ключу. ## 5. Ключи, привязанные к пользователям, упростили эксплуатацию {#sec-762242363071} Меня особенно порадовал тот факт, что обычные библиотеки вроде `rest_framework_api_key` предоставляют только сам ключ, но в моём случае **важно было соединить «ключ ↔ пользователь (клиент)»**. Мы унаследовали `AbstractAPIKey` и создали `CustomAPIKey`, связав её внешним ключом с моделью `AUTH_USER`: ```python from django.conf import settings from rest_framework_api_key.models import AbstractAPIKey from django.db import models class CustomAPIKey(AbstractAPIKey): user = models.ForeignKey( settings.AUTH_USER_MODEL, on_delete=models.CASCADE, related_name="api_keys" ) is_test = models.BooleanField(default=False) # для тестовых/стейджинг‑ключей ``` Эта привязка открыла массу возможностей, выходящих за рамки простой аутентификации. ## 6. Автоматическая выдача ключей per‑user и её операционные плюсы {#sec-d180d12a94f2} При регистрации пользователю автоматически создаётся ключ. Это дало несколько преимуществ. ### 1) Управление валидностью стало простым {#sec-bfe883fb0c50} - Можно быстро просмотреть, деактивировать или удалить ключ конкретного пользователя; - При удалении пользователя связанные ключи исчезают автоматически (FK cascade). ### 2) Ротация ключей стала тривиальной {#sec-7f4bccda836d} - При подозрении на компрометацию достаточно создать новый ключ и отозвать старый; - Поддержка нескольких ключей позволяет делать **безотказную замену** (новый ключ включён, старый постепенно выключается). ### 3) Тарифы/квоты/права теперь привязаны к пользователю {#sec-1081bea2890b} - Ограничения задаются не по ключу, а по пользователю, что упрощает логику биллинга; - Не нужно каждый раз определять, чей запрос стоит за конкретным ключом. ### 4) Один пользователь может иметь несколько ключей {#sec-1bc8fa524379} Флаг `is_test` показывает своё достоинство. Поскольку связь **не OneToOne**, а FK, один пользователь может иметь несколько ключей для разных целей: - **Тестовый** ключ для стейджинга; - **Продакшн**‑ключ для реального использования; - Разделение логов и мониторинга между тестовым и боевым трафиком. ## 7. Выбор аутентификационного механизма – не вопрос «лучшего», а вопрос «подходящего» {#sec-f73d5faf76f6} Итоги, которые я сейчас считаю оптимальными: - **OAuth2** – для интеграций с внешними сервисами, когда важен пользовательский consent; - **Сессионная аутентификация** – для одиночных Django‑приложений, где важна скорость разработки и простота; - **JWT** – для разделённых фронтенд/бэкенд, мобильных и SPA‑клиентов; - **API‑ключ** – для бекенд‑бекенд, автоматизации, воркеров и батч‑заданий, где запросы инициируются не пользователем. Как только в цепочку попадает Celery‑воркер, попытка «унифицировать всё через логин» только усложняет систему. Здесь API‑ключ стал идеальным спасением. ## 8. Заключение {#sec-d746e56908e8} Для людей (браузеров, приложений) естественно использовать сессии, JWT или OAuth2. Воркер же – процесс, которому нужно знать, **чей это запрос**. Мы перешли на API‑ключ не из-за громоздкой безопасности, а потому что это было самое простое решение в данном контексте. Привязав ключи к пользователям, мы превратили их из просто средства доступа в полноценный **операционный рычаг**. А вы часто используете API‑ключи? Я считаю, что даже ограниченное их применение может принести большую пользу. Надеюсь, мой опыт вдохновит вас на более гибкие решения. --- **Связанные статьи** - [Защита целостности сервер‑сервер запросов с HMAC‑подписями в Django/DRF](/ko/whitedec/2025/12/9/django-drf-hmac-signature-server-to-server-integrity/) - [Уроки React RCE: необходимость HMAC‑подписей, ротации ключей и нулевого доверия](/ko/whitedec/2025/12/8/react-rce-lesson-hmac-key-rotation-zero-trust/)