# Погружение в исходный код DRF: Почему я создавал только кастомные аутентификаторы и заново открыл для себя «встроенные» ## 1. Я люблю DRF, но не доверял встроенным аутентификаторам. {#sec-c2385e3e4312} Django и DRF (Django Rest Framework) — мои верные спутники в мире разработки. Однако когда дело доходило до аутентификации, у меня всегда был свой подход. Я предпочитаю использовать методы **JWT**, **API Key** или **OAuth2**, так как они являются стандартом в современных приложениях. Но что странно, в официальной документации DRF основное место занимают классы аутентификации, которые кажутся немного «устаревшими» по сравнению с современными методами, которые использую я, — это **Basic, Session, Token, Remote**. Я часто задавался вопросом: «Почему такой мощный фреймворк, как DRF, включает их в себя?» Поэтому я всегда наследовался от `BaseAuthentication` и создавал свои собственные кастомные аутентификаторы. ## 2. Зачем снова открывать исходный код «встроенных аутентификаторов»? {#sec-331b1362b7e2} Создавая кастомные аутентификаторы, я вдруг задумался: > «Действительно ли мой аутентификатор полностью соответствует философии Django и DRF?» Я начал беспокоиться, что, возможно, упускаю «естественность» схемы аутентификации, разработанной DRF, а не просто добавляю пользователя в `request.user`. Поэтому, начиная с сегодняшнего дня, я планирую подробно рассмотреть четыре встроенных аутентификатора DRF в нескольких статьях: * **BasicAuthentication** (базовая HTTP-аутентификация) * **SessionAuthentication** (использование сессий Django) * **TokenAuthentication** (простая токен-аутентификация) * **RemoteUserAuthentication** (интеграция с внешней аутентификацией) ## 3. В поисках «философии» за кажущейся «неудобностью» {#sec-e9f76a8db02e} Честно говоря, использовать `BasicAuthentication` в современных сервисах не очень удобно. Он кажется уязвимым с точки зрения безопасности, а постоянная отправка логина и пароля вызывает беспокойство. Однако, если заглянуть в его исходный код, можно обнаружить очень продуманный дизайн, касающийся **«того, как общаться с клиентом в случае неудачной аутентификации»**. Например, разница между простым возвратом `403 Forbidden` и инициированием стандартного `401 Unauthorized` через `authenticate_header`. Именно такие детали являются ключом к созданию «Django-подобных» классов. ## 4. Цель этой серии: сделать кастомные решения «естественными» {#sec-00b6ee52d1d1} В конце этой серии я стремлюсь получить не просто знания. 1. **Вдохновение (Inspiration):** Понять, почему встроенные аутентификаторы устроены именно так, и изучить их философию проектирования. 2. **Перенос (Porting):** Применить эту философию к моим любимым кастомным аутентификаторам, таким как JWT или OAuth2. 3. **Гармония (Harmony):** Сделать так, чтобы мои кастомные аутентификаторы работали в системе аутентификации DRF настолько естественно, будто они всегда там были. Это первый шаг к тому, чтобы стать разработчиком, который не просто реализует функции, но и способен воплотить философию фреймворка в коде. **Серия по анализу исходного кода аутентификаторов DRF** начинается прямо сейчас. ![개발자가 오래된 자물쇠를 해부하고 있는 이미지](/media/whitedec/blog_img/db4a2af117c3414395e86a0210d28f4a.webp) ## 5. Заглядываем в исходный код: `BasicAuthentication` {#sec-d3e96fecb1fa} Долгое вступление позади. Давайте приступим к изучению сердца **BasicAuthentication**. --- Открыв файл `rest_framework/authentication.py` в DRF, вы увидите этот класс. Он довольно лаконичен, но содержит прочные правила. ```python class BasicAuthentication(BaseAuthentication): """ HTTP Basic authentication against username/password. """ www_authenticate_realm = 'api' def authenticate(self, request): auth = get_authorization_header(request).split() if not auth or auth[0].lower() != b'basic': return None if len(auth) == 1: msg = _('Invalid basic header. No credentials provided.') raise exceptions.AuthenticationFailed(msg) elif len(auth) > 2: msg = _('Invalid basic header. Credentials string should not contain spaces.') raise exceptions.AuthenticationFailed(msg) try: try: auth_decoded = base64.b64decode(auth[1]).decode('utf-8') except UnicodeDecodeError: auth_decoded = base64.b64decode(auth[1]).decode('latin-1') userid, password = auth_decoded.split(':', 1) except (TypeError, ValueError, UnicodeDecodeError, binascii.Error): msg = _('Invalid basic header. Credentials not correctly base64 encoded.') raise exceptions.AuthenticationFailed(msg) return self.authenticate_credentials(userid, password, request) def authenticate_credentials(self, userid, password, request=None): credentials = { get_user_model().USERNAME_FIELD: userid, 'password': password } user = authenticate(request=request, **credentials) if user is None: raise exceptions.AuthenticationFailed(_('Invalid username/password.')) if not user.is_active: raise exceptions.AuthenticationFailed(_('User inactive or deleted.')) return (user, None) def authenticate_header(self, request): return 'Basic realm="%s"' % self.www_authenticate_realm ``` ### **Почему именно заголовок (Header)?** {#sec-d4f681d28784} Обычно, когда мы отправляем данные, мы в первую очередь думаем о `request.POST` или `request.data`. Однако для аутентификационной информации **заголовок** является общепринятым стандартом. Почему так? Аутентификация должна **гибко реагировать на все HTTP-методы (GET, POST, PUT, DELETE и т.д.)**. Запросы `GET` вообще не имеют тела, а `DELETE` часто также отправляется без него. Если бы нам пришлось помещать аутентификационные данные в тело запроса, то для обычных запросов на получение данных нам бы пришлось принудительно использовать POST, что привело бы к странному дизайну. Размещение аутентификации в заголовке — это своего рода обещание: **«Держите свой документ, удостоверяющий личность, при себе, независимо от того, какую дверь вы открываете»**. --- ### Суровая реальность: уязвимости безопасности {#sec-28526421f804} Главный недостаток этого метода — **Base64**. Это не шифрование, а просто «кодирование». Любой может узнать ваш логин и пароль за секунду, используя любой сайт для декодирования. > **Вывод:** Категорически запрещено использовать в HTTP-среде. Применяйте его только в безопасном туннеле HTTPS, и то лишь для очень ограниченных целей. --- ### Первое вдохновение: экономичная безопасность для межмашинного взаимодействия (M2M) {#sec-999544506ed4} Тем не менее, этот устаревший аутентификатор становится привлекательным в определённые моменты. А именно, **когда серверы обмениваются данными друг с другом**. Если ресурсы слишком ценны для создания HMAC или сложной системы API Key, но отправлять данные в чистом виде небезопасно, мы можем «слегка» изменить эту структуру. * **Кастомное шифрование:** Вместо Base64 передавайте бинарный контент, зашифрованный симметричным ключом, известным только обоим серверам. * **Простая реализация:** Быстрая и безопасная связь возможна без сложного рукопожатия, используя только формат `Basic `. Это становится эффективным каналом для обмена «паролем, известным только нам». --- ### **Второе вдохновение: потерянная шестерёнка `authenticate_header()`** {#sec-8f845e80ab36} Метод, на который стоит обратить особое внимание, — это `authenticate_header()`. Нигде в исходном коде он не вызывается напрямую. Однако он является **ключевым элементом для генерации '401 Unauthorized'** в обработчике исключений DRF. При создании кастомных аутентификаторов мы часто упускаем из виду этот метод. На самом деле, его отсутствие не влияет на сам акт **аутентификации**. Однако стоит сравнить, насколько эта незначительная разница может усложнить код на уровне View. #### **Случай 1. Когда `authenticate_header` опущен (ручной способ)** Если этот метод отсутствует, DRF считает, что «не может предоставить руководство по способу аутентификации», и в случае неудачи аутентификации возвращает `403 Forbidden` вместо `401`. Чтобы избежать этого, в View требуется следующая «ручная работа»: ```Python # ❌ Если authenticate_header отсутствует class MyPostAPIView(APIView): # Если использовать IsAuthenticated, всегда будет 403, поэтому для 401 пока разрешаем. permission_classes = [AllowAny] def post(self, request): # Необходимо вручную проверять статус аутентификации внутри View и возвращать 401. if not request.user or not request.user.is_authenticated: return Response( {"detail": "로그인이 필요합니다."}, status=status.HTTP_401_UNAUTHORIZED ) # ... Начало основной логики ... ``` #### **Случай 2. Когда `authenticate_header` реализован (реализация философии DRF)** С другой стороны, если в кастомном аутентификаторе указана всего одна строка (`authenticate_header`), обширная схема аутентификации DRF распознает это и автоматически формирует ответ `401`. ```Python # ✅ Если authenticate_header присутствует class MyPostAPIView(APIView): # Теперь DRF самостоятельно будет возвращать 401, что позволяет декларативно управлять правами. permission_classes = [IsAuthenticated] def post(self, request): # View фокусируется исключительно на «бизнес-логике». # Обработка ошибок аутентификации уже завершена на более высоком уровне (в методе authenticate_header аутентификатора). ... ``` Это и есть **«Django-подобная»** эстетика. Вместо того чтобы разбрасывать фрагментированный код по View, вы чувствуете, что идеально встраиваете свой код в шестерёнки, разработанные фреймворком. С помощью всего одного этого метода мы можем чётко разделить роли «аутентификации» и «авторизации», а также значительно облегчить View. --- ### Истинное очарование DRF: тихое отступление (эстетика `None`) {#sec-38c90c2a7f2a} И наконец, давайте ещё раз взглянем на этот фрагмент исходного кода. ```python if not auth or auth[0].lower() != b'basic': return None ``` Когда формат не соответствует, вместо того чтобы выдавать ошибку, он **возвращает `None` и тихо отступает**. Эта незначительная обработка создаёт гибкость DRF. Благодаря ей мы можем размещать несколько аутентификаторов в списке `AUTHENTICATION_CLASSES` по порядку. Разве вы не чувствуете заботу разработчиков, которые говорят: «Ах, это не мой метод? Тогда проверь следующий аутентификатор!»? --- ## Заключение {#sec-e57eed9058e2} Хотя `BasicAuthentication` может показаться устаревшим, его структура лежит в основе современных систем аутентификации. В частности, методы обработки исключений и поддержки множественной аутентификации — это ценные активы, которые мы обязательно должны перенять при создании собственных аутентификаторов. Что ж, мы преодолели первую вершину. В следующем посте мы подробно рассмотрим **`SessionAuthentication`**, который нам знаком, но всё ещё полон загадок. «То, чего вы не знали, используя сессионную аутентификацию», — ждите с нетерпением!