This commit is contained in:
@@ -1,19 +1,29 @@
|
||||
"""
|
||||
Middleware для обработки авторизации в GraphQL запросах
|
||||
"""
|
||||
|
||||
from typing import Any, Dict
|
||||
|
||||
from starlette.datastructures import Headers
|
||||
from starlette.requests import Request
|
||||
from starlette.responses import JSONResponse, Response
|
||||
from starlette.datastructures import Headers
|
||||
from starlette.types import ASGIApp, Scope, Receive, Send
|
||||
from starlette.types import ASGIApp, Receive, Scope, Send
|
||||
|
||||
from settings import (
|
||||
SESSION_COOKIE_HTTPONLY,
|
||||
SESSION_COOKIE_MAX_AGE,
|
||||
SESSION_COOKIE_NAME,
|
||||
SESSION_COOKIE_SAMESITE,
|
||||
SESSION_COOKIE_SECURE,
|
||||
SESSION_TOKEN_HEADER,
|
||||
)
|
||||
from utils.logger import root_logger as logger
|
||||
from settings import SESSION_COOKIE_HTTPONLY, SESSION_COOKIE_MAX_AGE, SESSION_COOKIE_SAMESITE, SESSION_COOKIE_SECURE, SESSION_TOKEN_HEADER, SESSION_COOKIE_NAME
|
||||
|
||||
|
||||
class AuthMiddleware:
|
||||
"""
|
||||
Универсальный middleware для обработки авторизации и управления cookies.
|
||||
|
||||
|
||||
Основные функции:
|
||||
1. Извлечение Bearer токена из заголовка Authorization или cookie
|
||||
2. Добавление токена в заголовки запроса для обработки AuthenticationMiddleware
|
||||
@@ -23,7 +33,7 @@ class AuthMiddleware:
|
||||
def __init__(self, app: ASGIApp):
|
||||
self.app = app
|
||||
self._context = None
|
||||
|
||||
|
||||
async def __call__(self, scope: Scope, receive: Receive, send: Send):
|
||||
"""Обработка ASGI запроса"""
|
||||
if scope["type"] != "http":
|
||||
@@ -93,33 +103,29 @@ class AuthMiddleware:
|
||||
scope["headers"] = new_headers
|
||||
|
||||
# Также добавляем информацию о типе аутентификации для дальнейшего использования
|
||||
scope["auth"] = {
|
||||
"type": "bearer",
|
||||
"token": token,
|
||||
"source": token_source
|
||||
}
|
||||
scope["auth"] = {"type": "bearer", "token": token, "source": token_source}
|
||||
logger.debug(f"[middleware] Токен добавлен в scope для аутентификации из источника: {token_source}")
|
||||
else:
|
||||
logger.debug(f"[middleware] Токен не найден ни в заголовке, ни в cookie")
|
||||
|
||||
await self.app(scope, receive, send)
|
||||
|
||||
|
||||
def set_context(self, context):
|
||||
"""Сохраняет ссылку на контекст GraphQL запроса"""
|
||||
self._context = context
|
||||
logger.debug(f"[middleware] Установлен контекст GraphQL: {bool(context)}")
|
||||
|
||||
|
||||
def set_cookie(self, key, value, **options):
|
||||
"""
|
||||
Устанавливает cookie в ответе
|
||||
|
||||
|
||||
Args:
|
||||
key: Имя cookie
|
||||
value: Значение cookie
|
||||
**options: Дополнительные параметры (httponly, secure, max_age, etc.)
|
||||
"""
|
||||
success = False
|
||||
|
||||
|
||||
# Способ 1: Через response
|
||||
if self._context and "response" in self._context and hasattr(self._context["response"], "set_cookie"):
|
||||
try:
|
||||
@@ -128,7 +134,7 @@ class AuthMiddleware:
|
||||
success = True
|
||||
except Exception as e:
|
||||
logger.error(f"[middleware] Ошибка при установке cookie {key} через response: {str(e)}")
|
||||
|
||||
|
||||
# Способ 2: Через собственный response в контексте
|
||||
if not success and hasattr(self, "_response") and self._response and hasattr(self._response, "set_cookie"):
|
||||
try:
|
||||
@@ -137,20 +143,20 @@ class AuthMiddleware:
|
||||
success = True
|
||||
except Exception as e:
|
||||
logger.error(f"[middleware] Ошибка при установке cookie {key} через _response: {str(e)}")
|
||||
|
||||
|
||||
if not success:
|
||||
logger.error(f"[middleware] Не удалось установить cookie {key}: объекты response недоступны")
|
||||
|
||||
def delete_cookie(self, key, **options):
|
||||
"""
|
||||
Удаляет cookie из ответа
|
||||
|
||||
|
||||
Args:
|
||||
key: Имя cookie для удаления
|
||||
**options: Дополнительные параметры
|
||||
"""
|
||||
success = False
|
||||
|
||||
|
||||
# Способ 1: Через response
|
||||
if self._context and "response" in self._context and hasattr(self._context["response"], "delete_cookie"):
|
||||
try:
|
||||
@@ -159,7 +165,7 @@ class AuthMiddleware:
|
||||
success = True
|
||||
except Exception as e:
|
||||
logger.error(f"[middleware] Ошибка при удалении cookie {key} через response: {str(e)}")
|
||||
|
||||
|
||||
# Способ 2: Через собственный response в контексте
|
||||
if not success and hasattr(self, "_response") and self._response and hasattr(self._response, "delete_cookie"):
|
||||
try:
|
||||
@@ -168,7 +174,7 @@ class AuthMiddleware:
|
||||
success = True
|
||||
except Exception as e:
|
||||
logger.error(f"[middleware] Ошибка при удалении cookie {key} через _response: {str(e)}")
|
||||
|
||||
|
||||
if not success:
|
||||
logger.error(f"[middleware] Не удалось удалить cookie {key}: объекты response недоступны")
|
||||
|
||||
@@ -180,38 +186,41 @@ class AuthMiddleware:
|
||||
try:
|
||||
# Получаем доступ к контексту запроса
|
||||
context = info.context
|
||||
|
||||
|
||||
# Сохраняем ссылку на контекст
|
||||
self.set_context(context)
|
||||
|
||||
|
||||
# Добавляем себя как объект, содержащий утилитные методы
|
||||
context["extensions"] = self
|
||||
|
||||
|
||||
# Проверяем наличие response в контексте
|
||||
if "response" not in context or not context["response"]:
|
||||
from starlette.responses import JSONResponse
|
||||
|
||||
context["response"] = JSONResponse({})
|
||||
logger.debug("[middleware] Создан новый response объект в контексте GraphQL")
|
||||
|
||||
logger.debug(f"[middleware] GraphQL resolve: контекст подготовлен, добавлены расширения для работы с cookie")
|
||||
|
||||
|
||||
logger.debug(
|
||||
f"[middleware] GraphQL resolve: контекст подготовлен, добавлены расширения для работы с cookie"
|
||||
)
|
||||
|
||||
return await next(root, info, *args, **kwargs)
|
||||
except Exception as e:
|
||||
logger.error(f"[AuthMiddleware] Ошибка в GraphQL resolve: {str(e)}")
|
||||
raise
|
||||
|
||||
|
||||
async def process_result(self, request: Request, result: Any) -> Response:
|
||||
"""
|
||||
Обрабатывает результат GraphQL запроса, поддерживая установку cookie
|
||||
|
||||
|
||||
Args:
|
||||
request: Starlette Request объект
|
||||
result: результат GraphQL запроса (dict или Response)
|
||||
|
||||
|
||||
Returns:
|
||||
Response: HTTP-ответ с результатом и cookie (если необходимо)
|
||||
"""
|
||||
|
||||
|
||||
# Проверяем, является ли result уже объектом Response
|
||||
if isinstance(result, Response):
|
||||
response = result
|
||||
@@ -220,19 +229,20 @@ class AuthMiddleware:
|
||||
if isinstance(result, JSONResponse):
|
||||
try:
|
||||
import json
|
||||
result_data = json.loads(result.body.decode('utf-8'))
|
||||
|
||||
result_data = json.loads(result.body.decode("utf-8"))
|
||||
except Exception as e:
|
||||
logger.error(f"[process_result] Не удалось извлечь данные из JSONResponse: {str(e)}")
|
||||
else:
|
||||
response = JSONResponse(result)
|
||||
result_data = result
|
||||
|
||||
|
||||
# Проверяем, был ли токен в запросе или ответе
|
||||
if request.method == "POST":
|
||||
try:
|
||||
data = await request.json()
|
||||
op_name = data.get("operationName", "").lower()
|
||||
|
||||
|
||||
# Если это операция логина или обновления токена, и в ответе есть токен
|
||||
if op_name in ["login", "refreshtoken"]:
|
||||
token = None
|
||||
@@ -243,32 +253,35 @@ class AuthMiddleware:
|
||||
op_result = data_obj.get(op_name, {})
|
||||
if isinstance(op_result, dict) and "token" in op_result:
|
||||
token = op_result.get("token")
|
||||
|
||||
|
||||
if token:
|
||||
# Устанавливаем cookie с токеном
|
||||
response.set_cookie(
|
||||
key=SESSION_COOKIE_NAME,
|
||||
value=token,
|
||||
httponly=SESSION_COOKIE_HTTPONLY,
|
||||
httponly=SESSION_COOKIE_HTTPONLY,
|
||||
secure=SESSION_COOKIE_SECURE,
|
||||
samesite=SESSION_COOKIE_SAMESITE,
|
||||
max_age=SESSION_COOKIE_MAX_AGE,
|
||||
)
|
||||
logger.debug(f"[graphql_handler] Установлена cookie {SESSION_COOKIE_NAME} для операции {op_name}")
|
||||
|
||||
logger.debug(
|
||||
f"[graphql_handler] Установлена cookie {SESSION_COOKIE_NAME} для операции {op_name}"
|
||||
)
|
||||
|
||||
# Если это операция logout, удаляем cookie
|
||||
elif op_name == "logout":
|
||||
response.delete_cookie(
|
||||
key=SESSION_COOKIE_NAME,
|
||||
secure=SESSION_COOKIE_SECURE,
|
||||
httponly=SESSION_COOKIE_HTTPONLY,
|
||||
samesite=SESSION_COOKIE_SAMESITE
|
||||
samesite=SESSION_COOKIE_SAMESITE,
|
||||
)
|
||||
logger.debug(f"[graphql_handler] Удалена cookie {SESSION_COOKIE_NAME} для операции {op_name}")
|
||||
except Exception as e:
|
||||
logger.error(f"[process_result] Ошибка при обработке POST запроса: {str(e)}")
|
||||
|
||||
|
||||
return response
|
||||
|
||||
|
||||
|
||||
# Создаем единый экземпляр AuthMiddleware для использования с GraphQL
|
||||
auth_middleware = AuthMiddleware(lambda scope, receive, send: None)
|
||||
auth_middleware = AuthMiddleware(lambda scope, receive, send: None)
|
||||
|
Reference in New Issue
Block a user