Поговорим о реализации аутентификации в веб-сервисе, построенном с использованием Django Rest Framework (DRF). Мы столкнулись с вызовом при поддержке двух методов аутентификации: токены DRF и OpenID Connect (OIDC) из Keycloak. Пересматривать всю логику авторизации и внедрять ее с нуля было непрактично в свете принципов SOLID. В данном контексте мы посмотрим на сохранение логики проверки токенов в базовых классах и использование Middleware для поддержки аутентификации через Websocket.
Проблема
Существующий сервис, разработанный на основе DRF, предоставлял два метода аутентификации: токены DRF и OpenID Connect (OIDC) из Keycloak. Возможность пересмотра логики с нуля была исключена в связи с принципами SOLID. Наша задача заключалась в сохранении основных классов и добавлении поддержки нового метода аутентификации через Websocket.
Решение
Процесс реализации сводился к внедрению Middleware, который расширял текущую логику аутентификации в DRF. Давайте разберемся с этим пошагово.
Определение роутов
Начнем с определения роутов, которые нужно обернуть в созданный Middleware для обеспечения аутентификации через Websocket.
websocket_urlpatterns = TelebackAuthMiddlewareStack( URLRouter([ re_path(r"^notifications$", consumers.NotificationsConsumer.as_asgi(), name="ws_notifications", ), ]) )
Создание Middleware
Новый уровень аутентификации был добавлен через создание Middleware.
class AsyncTokenAuthentication(TokenAuthentication): async def authenticate(self, request): return await sync_to_async(super().authenticate)(request) class AsyncOIDCAuthentication(OIDCAuthentication): async def authenticate(self, request, **kwargs): return await sync_to_async(super().authenticate)(request) def make_request_of_scope(scope): request = HttpRequest() headers = scope.get('headers', []) auth_key = b'authorization' for name, value in headers: if name == auth_key: request.META["HTTP_AUTHORIZATION"] = value return request class MakeRequestMiddleware: def __init__(self, app): self.app = app async def __call__(self, scope, receive, send): scope['request'] = make_request_of_scope(scope) return await self.app(scope, receive, send) class TelebackOIDCAuthMiddleware: def __init__(self, app): self.app = app async def __call__(self, scope, receive, send): try: user, _ = await AsyncOIDCAuthentication().authenticate(scope['request']) scope['user'] = user except (Exception,): pass return await self.app(scope, receive, send) class TokenAuthMiddleware: def __init__(self, app): self.app = app async def __call__(self, scope, receive, send): try: user, _ = await AsyncTokenAuthentication().authenticate(scope['request']) scope['user'] = user except (Exception,): pass return await self.app(scope, receive, send) def TelebackAuthMiddlewareStack(inner): return MakeRequestMiddleware(TokenAuthMiddleware(TelebackOIDCAuthMiddleware(inner)))
В нашем веб-сервисе, построенном на библиотеке Channels (https://channels.readthedocs.io/en/latest/) с использованием ASGI, нам потребовалось асинхронное выполнение методов аутентификации. Особое внимание уделяется асинхронному подходу при проверке токена через AsyncTokenAuthentication и AsyncOIDCAuthentication. В этом контексте рассмотрим оптимизацию функции make_request_of_scope, которая теперь создает HttpRequest асинхронно и добавляет заголовок для аутентификации. Мы также внедрили отдельный посредник MakeRequestMiddleware для повторного использования request. В заключении, аутентификацию вызываем через TelebackOIDCAuthMiddleware и TokenAuthMiddleware, сохраняя результат в scope для будущего использования.