Погружение в исходный код DRF: Почему я создавал только кастомные аутентификаторы и заново открыл для себя «встроенные»

1. Я люблю DRF, но не доверял встроенным аутентификаторам.



Django и DRF (Django Rest Framework) — мои верные спутники в мире разработки. Однако когда дело доходило до аутентификации, у меня всегда был свой подход. Я предпочитаю использовать методы JWT, API Key или OAuth2, так как они являются стандартом в современных приложениях.

Но что странно, в официальной документации DRF основное место занимают классы аутентификации, которые кажутся немного «устаревшими» по сравнению с современными методами, которые использую я, — это Basic, Session, Token, Remote. Я часто задавался вопросом: «Почему такой мощный фреймворк, как DRF, включает их в себя?» Поэтому я всегда наследовался от BaseAuthentication и создавал свои собственные кастомные аутентификаторы.

2. Зачем снова открывать исходный код «встроенных аутентификаторов»?

Создавая кастомные аутентификаторы, я вдруг задумался:

«Действительно ли мой аутентификатор полностью соответствует философии Django и DRF?»

Я начал беспокоиться, что, возможно, упускаю «естественность» схемы аутентификации, разработанной DRF, а не просто добавляю пользователя в request.user. Поэтому, начиная с сегодняшнего дня, я планирую подробно рассмотреть четыре встроенных аутентификатора DRF в нескольких статьях:

  • BasicAuthentication (базовая HTTP-аутентификация)
  • SessionAuthentication (использование сессий Django)
  • TokenAuthentication (простая токен-аутентификация)
  • RemoteUserAuthentication (интеграция с внешней аутентификацией)

3. В поисках «философии» за кажущейся «неудобностью»



Честно говоря, использовать BasicAuthentication в современных сервисах не очень удобно. Он кажется уязвимым с точки зрения безопасности, а постоянная отправка логина и пароля вызывает беспокойство. Однако, если заглянуть в его исходный код, можно обнаружить очень продуманный дизайн, касающийся «того, как общаться с клиентом в случае неудачной аутентификации».

Например, разница между простым возвратом 403 Forbidden и инициированием стандартного 401 Unauthorized через authenticate_header. Именно такие детали являются ключом к созданию «Django-подобных» классов.

4. Цель этой серии: сделать кастомные решения «естественными»

В конце этой серии я стремлюсь получить не просто знания.

  1. Вдохновение (Inspiration): Понять, почему встроенные аутентификаторы устроены именно так, и изучить их философию проектирования.
  2. Перенос (Porting): Применить эту философию к моим любимым кастомным аутентификаторам, таким как JWT или OAuth2.
  3. Гармония (Harmony): Сделать так, чтобы мои кастомные аутентификаторы работали в системе аутентификации DRF настолько естественно, будто они всегда там были.

Это первый шаг к тому, чтобы стать разработчиком, который не просто реализует функции, но и способен воплотить философию фреймворка в коде. Серия по анализу исходного кода аутентификаторов DRF начинается прямо сейчас.

개발자가 오래된 자물쇠를 해부하고 있는 이미지

5. Заглядываем в исходный код: BasicAuthentication

Долгое вступление позади. Давайте приступим к изучению сердца BasicAuthentication.


Открыв файл rest_framework/authentication.py в DRF, вы увидите этот класс. Он довольно лаконичен, но содержит прочные правила.

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)?

Обычно, когда мы отправляем данные, мы в первую очередь думаем о request.POST или request.data. Однако для аутентификационной информации заголовок является общепринятым стандартом. Почему так?

Аутентификация должна гибко реагировать на все HTTP-методы (GET, POST, PUT, DELETE и т.д.). Запросы GET вообще не имеют тела, а DELETE часто также отправляется без него. Если бы нам пришлось помещать аутентификационные данные в тело запроса, то для обычных запросов на получение данных нам бы пришлось принудительно использовать POST, что привело бы к странному дизайну. Размещение аутентификации в заголовке — это своего рода обещание: «Держите свой документ, удостоверяющий личность, при себе, независимо от того, какую дверь вы открываете».


Суровая реальность: уязвимости безопасности

Главный недостаток этого метода — Base64. Это не шифрование, а просто «кодирование». Любой может узнать ваш логин и пароль за секунду, используя любой сайт для декодирования.

Вывод: Категорически запрещено использовать в HTTP-среде. Применяйте его только в безопасном туннеле HTTPS, и то лишь для очень ограниченных целей.


Первое вдохновение: экономичная безопасность для межмашинного взаимодействия (M2M)

Тем не менее, этот устаревший аутентификатор становится привлекательным в определённые моменты. А именно, когда серверы обмениваются данными друг с другом.

Если ресурсы слишком ценны для создания HMAC или сложной системы API Key, но отправлять данные в чистом виде небезопасно, мы можем «слегка» изменить эту структуру.

  • Кастомное шифрование: Вместо Base64 передавайте бинарный контент, зашифрованный симметричным ключом, известным только обоим серверам.
  • Простая реализация: Быстрая и безопасная связь возможна без сложного рукопожатия, используя только формат Basic <Encrypted_Data>. Это становится эффективным каналом для обмена «паролем, известным только нам».

Второе вдохновение: потерянная шестерёнка authenticate_header()

Метод, на который стоит обратить особое внимание, — это authenticate_header(). Нигде в исходном коде он не вызывается напрямую. Однако он является ключевым элементом для генерации '401 Unauthorized' в обработчике исключений DRF.

При создании кастомных аутентификаторов мы часто упускаем из виду этот метод. На самом деле, его отсутствие не влияет на сам акт аутентификации. Однако стоит сравнить, насколько эта незначительная разница может усложнить код на уровне View.

Случай 1. Когда authenticate_header опущен (ручной способ)

Если этот метод отсутствует, DRF считает, что «не может предоставить руководство по способу аутентификации», и в случае неудачи аутентификации возвращает 403 Forbidden вместо 401. Чтобы избежать этого, в View требуется следующая «ручная работа»:

# ❌ Если 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.

# ✅ Если authenticate_header присутствует
class MyPostAPIView(APIView):
    # Теперь DRF самостоятельно будет возвращать 401, что позволяет декларативно управлять правами.
    permission_classes = [IsAuthenticated] 

    def post(self, request):
        # View фокусируется исключительно на «бизнес-логике».
        # Обработка ошибок аутентификации уже завершена на более высоком уровне (в методе authenticate_header аутентификатора).
        ... 

Это и есть «Django-подобная» эстетика. Вместо того чтобы разбрасывать фрагментированный код по View, вы чувствуете, что идеально встраиваете свой код в шестерёнки, разработанные фреймворком. С помощью всего одного этого метода мы можем чётко разделить роли «аутентификации» и «авторизации», а также значительно облегчить View.


Истинное очарование DRF: тихое отступление (эстетика None)

И наконец, давайте ещё раз взглянем на этот фрагмент исходного кода.

if not auth or auth[0].lower() != b'basic':
    return None

Когда формат не соответствует, вместо того чтобы выдавать ошибку, он возвращает None и тихо отступает. Эта незначительная обработка создаёт гибкость DRF. Благодаря ей мы можем размещать несколько аутентификаторов в списке AUTHENTICATION_CLASSES по порядку. Разве вы не чувствуете заботу разработчиков, которые говорят: «Ах, это не мой метод? Тогда проверь следующий аутентификатор!»?


Заключение

Хотя BasicAuthentication может показаться устаревшим, его структура лежит в основе современных систем аутентификации. В частности, методы обработки исключений и поддержки множественной аутентификации — это ценные активы, которые мы обязательно должны перенять при создании собственных аутентификаторов.

Что ж, мы преодолели первую вершину. В следующем посте мы подробно рассмотрим SessionAuthentication, который нам знаком, но всё ещё полон загадок.

«То, чего вы не знали, используя сессионную аутентификацию», — ждите с нетерпением!