INTERNAL AUTH FIX
This commit is contained in:
parent
f6156ccfa3
commit
ebf9dfcf62
|
@ -167,7 +167,9 @@ async def validate_graphql_context(info: Any) -> None:
|
||||||
auth_cred = request.scope.get("auth")
|
auth_cred = request.scope.get("auth")
|
||||||
if isinstance(auth_cred, AuthCredentials) and auth_cred.logged_in:
|
if isinstance(auth_cred, AuthCredentials) and auth_cred.logged_in:
|
||||||
logger.debug(f"[decorators] Пользователь авторизован через scope: {auth_cred.author_id}")
|
logger.debug(f"[decorators] Пользователь авторизован через scope: {auth_cred.author_id}")
|
||||||
# В этом случае мы не делаем return, чтобы также проверить токен если нужно
|
# Устанавливаем auth в request для дальнейшего использования
|
||||||
|
request.auth = auth_cred
|
||||||
|
return
|
||||||
|
|
||||||
# Если авторизации нет ни в auth, ни в scope, пробуем получить и проверить токен
|
# Если авторизации нет ни в auth, ни в scope, пробуем получить и проверить токен
|
||||||
token = get_auth_token(request)
|
token = get_auth_token(request)
|
||||||
|
@ -188,9 +190,29 @@ async def validate_graphql_context(info: Any) -> None:
|
||||||
logger.warning(f"[decorators] Недействительный токен: {error_msg}")
|
logger.warning(f"[decorators] Недействительный токен: {error_msg}")
|
||||||
raise GraphQLError(f"Unauthorized - {error_msg}")
|
raise GraphQLError(f"Unauthorized - {error_msg}")
|
||||||
|
|
||||||
# Если все проверки пройдены, оставляем AuthState в scope
|
# Если все проверки пройдены, создаем AuthCredentials и устанавливаем в request.auth
|
||||||
# AuthenticationMiddleware извлечет нужные данные оттуда при необходимости
|
with local_session() as session:
|
||||||
logger.debug(f"[decorators] Токен успешно проверен для пользователя {auth_state.author_id}")
|
try:
|
||||||
|
author = session.query(Author).filter(Author.id == auth_state.author_id).one()
|
||||||
|
# Получаем разрешения из ролей
|
||||||
|
scopes = author.get_permissions()
|
||||||
|
|
||||||
|
# Создаем объект авторизации
|
||||||
|
auth_cred = AuthCredentials(
|
||||||
|
author_id=author.id,
|
||||||
|
scopes=scopes,
|
||||||
|
logged_in=True,
|
||||||
|
email=author.email,
|
||||||
|
token=auth_state.token
|
||||||
|
)
|
||||||
|
|
||||||
|
# Устанавливаем auth в request
|
||||||
|
request.auth = auth_cred
|
||||||
|
logger.debug(f"[decorators] Токен успешно проверен и установлен для пользователя {auth_state.author_id}")
|
||||||
|
except exc.NoResultFound:
|
||||||
|
logger.error(f"[decorators] Пользователь с ID {auth_state.author_id} не найден в базе данных")
|
||||||
|
raise GraphQLError("Unauthorized - user not found")
|
||||||
|
|
||||||
return
|
return
|
||||||
|
|
||||||
|
|
||||||
|
@ -203,7 +225,7 @@ def admin_auth_required(resolver: Callable) -> Callable:
|
||||||
resolver: GraphQL резолвер для защиты
|
resolver: GraphQL резолвер для защиты
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
Обернутый резолвер, который проверяет права доступа
|
Обернутый резолвер, который проверяет права доступа администратора
|
||||||
|
|
||||||
Raises:
|
Raises:
|
||||||
GraphQLError: если пользователь не авторизован или не имеет доступа администратора
|
GraphQLError: если пользователь не авторизован или не имеет доступа администратора
|
||||||
|
@ -216,9 +238,16 @@ def admin_auth_required(resolver: Callable) -> Callable:
|
||||||
@wraps(resolver)
|
@wraps(resolver)
|
||||||
async def wrapper(root: Any = None, info: Any = None, **kwargs):
|
async def wrapper(root: Any = None, info: Any = None, **kwargs):
|
||||||
try:
|
try:
|
||||||
|
# Проверяем авторизацию пользователя
|
||||||
await validate_graphql_context(info)
|
await validate_graphql_context(info)
|
||||||
|
|
||||||
|
# Получаем объект авторизации
|
||||||
auth = info.context["request"].auth
|
auth = info.context["request"].auth
|
||||||
|
if not auth or not auth.logged_in:
|
||||||
|
logger.error(f"[admin_auth_required] Пользователь не авторизован после validate_graphql_context")
|
||||||
|
raise GraphQLError("Unauthorized - please login")
|
||||||
|
|
||||||
|
# Проверяем, является ли пользователь администратором
|
||||||
with local_session() as session:
|
with local_session() as session:
|
||||||
try:
|
try:
|
||||||
# Преобразуем author_id в int для совместимости с базой данных
|
# Преобразуем author_id в int для совместимости с базой данных
|
||||||
|
@ -229,11 +258,20 @@ def admin_auth_required(resolver: Callable) -> Callable:
|
||||||
|
|
||||||
author = session.query(Author).filter(Author.id == author_id).one()
|
author = session.query(Author).filter(Author.id == author_id).one()
|
||||||
|
|
||||||
|
# Проверяем, является ли пользователь администратором
|
||||||
if author.email in ADMIN_EMAILS:
|
if author.email in ADMIN_EMAILS:
|
||||||
logger.info(f"Admin access granted for {author.email} (ID: {author.id})")
|
logger.info(f"Admin access granted for {author.email} (ID: {author.id})")
|
||||||
return await resolver(root, info, **kwargs)
|
return await resolver(root, info, **kwargs)
|
||||||
|
|
||||||
logger.warning(f"Admin access denied for {author.email} (ID: {author.id})")
|
# Проверяем роли пользователя
|
||||||
|
admin_roles = ['admin', 'super']
|
||||||
|
user_roles = [role.id for role in author.roles] if author.roles else []
|
||||||
|
|
||||||
|
if any(role in admin_roles for role in user_roles):
|
||||||
|
logger.info(f"Admin access granted for {author.email} (ID: {author.id}) with role: {user_roles}")
|
||||||
|
return await resolver(root, info, **kwargs)
|
||||||
|
|
||||||
|
logger.warning(f"Admin access denied for {author.email} (ID: {author.id}). Roles: {user_roles}")
|
||||||
raise GraphQLError("Unauthorized - not an admin")
|
raise GraphQLError("Unauthorized - not an admin")
|
||||||
except exc.NoResultFound:
|
except exc.NoResultFound:
|
||||||
logger.error(f"[admin_auth_required] Пользователь с ID {auth.author_id} не найден в базе данных")
|
logger.error(f"[admin_auth_required] Пользователь с ID {auth.author_id} не найден в базе данных")
|
||||||
|
@ -249,8 +287,6 @@ def admin_auth_required(resolver: Callable) -> Callable:
|
||||||
return wrapper
|
return wrapper
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
def permission_required(resource: str, operation: str, func):
|
def permission_required(resource: str, operation: str, func):
|
||||||
"""
|
"""
|
||||||
Декоратор для проверки разрешений.
|
Декоратор для проверки разрешений.
|
||||||
|
@ -263,43 +299,99 @@ def permission_required(resource: str, operation: str, func):
|
||||||
|
|
||||||
@wraps(func)
|
@wraps(func)
|
||||||
async def wrap(parent, info: GraphQLResolveInfo, *args, **kwargs):
|
async def wrap(parent, info: GraphQLResolveInfo, *args, **kwargs):
|
||||||
auth: AuthCredentials = info.context["request"].auth
|
# Сначала проверяем авторизацию
|
||||||
if not auth.logged_in:
|
await validate_graphql_context(info)
|
||||||
raise OperationNotAllowed(auth.error_message or "Please login")
|
|
||||||
|
# Получаем объект авторизации
|
||||||
|
logger.debug(f"[permission_required] Контекст: {info.context}")
|
||||||
|
auth = info.context["request"].auth
|
||||||
|
if not auth or not auth.logged_in:
|
||||||
|
logger.error(f"[permission_required] Пользователь не авторизован после validate_graphql_context")
|
||||||
|
raise OperationNotAllowed("Требуются права доступа")
|
||||||
|
|
||||||
|
# Проверяем разрешения
|
||||||
with local_session() as session:
|
with local_session() as session:
|
||||||
author = session.query(Author).filter(Author.id == auth.author_id).one()
|
try:
|
||||||
|
author = session.query(Author).filter(Author.id == auth.author_id).one()
|
||||||
|
|
||||||
# Проверяем базовые условия
|
# Проверяем базовые условия
|
||||||
if not author.is_active:
|
if not author.is_active:
|
||||||
raise OperationNotAllowed("Account is not active")
|
raise OperationNotAllowed("Account is not active")
|
||||||
if author.is_locked():
|
if author.is_locked():
|
||||||
raise OperationNotAllowed("Account is locked")
|
raise OperationNotAllowed("Account is locked")
|
||||||
|
|
||||||
# Проверяем разрешение
|
# Проверяем, является ли пользователь администратором (у них есть все разрешения)
|
||||||
if not author.has_permission(resource, operation):
|
if author.email in ADMIN_EMAILS:
|
||||||
raise OperationNotAllowed(f"No permission for {operation} on {resource}")
|
logger.debug(f"[permission_required] Администратор {author.email} имеет все разрешения")
|
||||||
|
return await func(parent, info, *args, **kwargs)
|
||||||
|
|
||||||
|
# Проверяем роли пользователя
|
||||||
|
admin_roles = ['admin', 'super']
|
||||||
|
user_roles = [role.id for role in author.roles] if author.roles else []
|
||||||
|
|
||||||
|
if any(role in admin_roles for role in user_roles):
|
||||||
|
logger.debug(f"[permission_required] Пользователь с ролью администратора {author.email} имеет все разрешения")
|
||||||
|
return await func(parent, info, *args, **kwargs)
|
||||||
|
|
||||||
return await func(parent, info, *args, **kwargs)
|
# Проверяем разрешение
|
||||||
|
if not author.has_permission(resource, operation):
|
||||||
|
logger.warning(f"[permission_required] У пользователя {author.email} нет разрешения {operation} на {resource}")
|
||||||
|
raise OperationNotAllowed(f"No permission for {operation} on {resource}")
|
||||||
|
|
||||||
|
logger.debug(f"[permission_required] Пользователь {author.email} имеет разрешение {operation} на {resource}")
|
||||||
|
return await func(parent, info, *args, **kwargs)
|
||||||
|
except exc.NoResultFound:
|
||||||
|
logger.error(f"[permission_required] Пользователь с ID {auth.author_id} не найден в базе данных")
|
||||||
|
raise OperationNotAllowed("User not found")
|
||||||
|
|
||||||
return wrap
|
return wrap
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
def login_accepted(func):
|
def login_accepted(func):
|
||||||
|
"""
|
||||||
|
Декоратор для резолверов, которые могут работать как с авторизованными,
|
||||||
|
так и с неавторизованными пользователями.
|
||||||
|
|
||||||
|
Добавляет информацию о пользователе в контекст, если пользователь авторизован.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
func: Декорируемая функция
|
||||||
|
"""
|
||||||
@wraps(func)
|
@wraps(func)
|
||||||
async def wrap(parent, info: GraphQLResolveInfo, *args, **kwargs):
|
async def wrap(parent, info: GraphQLResolveInfo, *args, **kwargs):
|
||||||
auth: AuthCredentials = info.context["request"].auth
|
try:
|
||||||
|
# Пробуем проверить авторизацию, но не выбрасываем исключение, если пользователь не авторизован
|
||||||
|
try:
|
||||||
|
await validate_graphql_context(info)
|
||||||
|
except GraphQLError:
|
||||||
|
# Игнорируем ошибку авторизации
|
||||||
|
pass
|
||||||
|
|
||||||
|
# Получаем объект авторизации
|
||||||
|
auth = getattr(info.context["request"], "auth", None)
|
||||||
|
|
||||||
|
if auth and auth.logged_in:
|
||||||
|
# Если пользователь авторизован, добавляем информацию о нем в контекст
|
||||||
|
with local_session() as session:
|
||||||
|
try:
|
||||||
|
author = session.query(Author).filter(Author.id == auth.author_id).one()
|
||||||
|
info.context["author"] = author.dict()
|
||||||
|
info.context["user_id"] = author.id
|
||||||
|
logger.debug(f"[login_accepted] Пользователь авторизован: {author.id}")
|
||||||
|
except exc.NoResultFound:
|
||||||
|
logger.warning(f"[login_accepted] Пользователь с ID {auth.author_id} не найден в базе данных")
|
||||||
|
info.context["author"] = None
|
||||||
|
info.context["user_id"] = None
|
||||||
|
else:
|
||||||
|
# Если пользователь не авторизован, устанавливаем пустые значения
|
||||||
|
info.context["author"] = None
|
||||||
|
info.context["user_id"] = None
|
||||||
|
logger.debug("[login_accepted] Пользователь не авторизован")
|
||||||
|
|
||||||
if auth and auth.logged_in:
|
return await func(parent, info, *args, **kwargs)
|
||||||
with local_session() as session:
|
except Exception as e:
|
||||||
author = session.query(Author).filter(Author.id == auth.author_id).one()
|
if not isinstance(e, GraphQLError):
|
||||||
info.context["author"] = author.dict()
|
logger.error(f"[login_accepted] Ошибка: {e}")
|
||||||
info.context["user_id"] = author.id
|
raise
|
||||||
else:
|
|
||||||
info.context["author"] = None
|
|
||||||
info.context["user_id"] = None
|
|
||||||
|
|
||||||
return await func(parent, info, *args, **kwargs)
|
|
||||||
|
|
||||||
return wrap
|
return wrap
|
||||||
|
|
|
@ -162,15 +162,20 @@ async def verify_internal_auth(token: str) -> Tuple[str, list, bool]:
|
||||||
Returns:
|
Returns:
|
||||||
tuple: (user_id, roles, is_admin)
|
tuple: (user_id, roles, is_admin)
|
||||||
"""
|
"""
|
||||||
|
logger.debug(f"[verify_internal_auth] Проверка токена: {token[:10]}...")
|
||||||
|
|
||||||
# Обработка формата "Bearer <token>" (если токен не был обработан ранее)
|
# Обработка формата "Bearer <token>" (если токен не был обработан ранее)
|
||||||
if token.startswith("Bearer "):
|
if token and token.startswith("Bearer "):
|
||||||
token = token.replace("Bearer ", "", 1).strip()
|
token = token.replace("Bearer ", "", 1).strip()
|
||||||
|
|
||||||
# Проверяем сессию
|
# Проверяем сессию
|
||||||
payload = await SessionManager.verify_session(token)
|
payload = await SessionManager.verify_session(token)
|
||||||
if not payload:
|
if not payload:
|
||||||
|
logger.warning("[verify_internal_auth] Недействительный токен: payload не получен")
|
||||||
return "", [], False
|
return "", [], False
|
||||||
|
|
||||||
|
logger.debug(f"[verify_internal_auth] Токен действителен, user_id={payload.user_id}")
|
||||||
|
|
||||||
with local_session() as session:
|
with local_session() as session:
|
||||||
try:
|
try:
|
||||||
author = (
|
author = (
|
||||||
|
@ -182,12 +187,15 @@ async def verify_internal_auth(token: str) -> Tuple[str, list, bool]:
|
||||||
|
|
||||||
# Получаем роли
|
# Получаем роли
|
||||||
roles = [role.id for role in author.roles]
|
roles = [role.id for role in author.roles]
|
||||||
|
logger.debug(f"[verify_internal_auth] Роли пользователя: {roles}")
|
||||||
|
|
||||||
# Определяем, является ли пользователь администратором
|
# Определяем, является ли пользователь администратором
|
||||||
is_admin = any(role in ['admin', 'super'] for role in roles) or author.email in ADMIN_EMAILS
|
is_admin = any(role in ['admin', 'super'] for role in roles) or author.email in ADMIN_EMAILS
|
||||||
|
logger.debug(f"[verify_internal_auth] Пользователь {author.id} {'является' if is_admin else 'не является'} администратором")
|
||||||
|
|
||||||
return str(author.id), roles, is_admin
|
return str(author.id), roles, is_admin
|
||||||
except exc.NoResultFound:
|
except exc.NoResultFound:
|
||||||
|
logger.warning(f"[verify_internal_auth] Пользователь с ID {payload.user_id} не найден в БД или не активен")
|
||||||
return "", [], False
|
return "", [], False
|
||||||
|
|
||||||
|
|
||||||
|
@ -284,6 +292,31 @@ async def authenticate(request: Any) -> AuthState:
|
||||||
state.token = token
|
state.token = token
|
||||||
state.username = payload.username
|
state.username = payload.username
|
||||||
|
|
||||||
|
# Если запрос имеет атрибут auth, устанавливаем в него авторизационные данные
|
||||||
|
if hasattr(request, "auth") or hasattr(request, "__setattr__"):
|
||||||
|
try:
|
||||||
|
# Получаем информацию о пользователе для создания AuthCredentials
|
||||||
|
with local_session() as session:
|
||||||
|
author = session.query(Author).filter(Author.id == payload.user_id).one_or_none()
|
||||||
|
if author:
|
||||||
|
# Получаем разрешения из ролей
|
||||||
|
scopes = author.get_permissions()
|
||||||
|
|
||||||
|
# Создаем объект авторизации
|
||||||
|
auth_cred = AuthCredentials(
|
||||||
|
author_id=author.id,
|
||||||
|
scopes=scopes,
|
||||||
|
logged_in=True,
|
||||||
|
email=author.email,
|
||||||
|
token=token
|
||||||
|
)
|
||||||
|
|
||||||
|
# Устанавливаем auth в request
|
||||||
|
setattr(request, "auth", auth_cred)
|
||||||
|
logger.debug(f"[auth.authenticate] Авторизационные данные установлены в request.auth для {payload.user_id}")
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"[auth.authenticate] Ошибка при установке auth в request: {e}")
|
||||||
|
|
||||||
logger.info(f"[auth.authenticate] Успешная аутентификация пользователя {state.author_id}")
|
logger.info(f"[auth.authenticate] Успешная аутентификация пользователя {state.author_id}")
|
||||||
|
|
||||||
return state
|
return state
|
||||||
|
|
|
@ -66,8 +66,11 @@ class JWTCodec:
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def decode(token: str, verify_exp: bool = True):
|
def decode(token: str, verify_exp: bool = True):
|
||||||
logger.debug(f"[JWTCodec.decode] Начало декодирования токена длиной {len(token) if token else 0}")
|
logger.debug(f"[JWTCodec.decode] Начало декодирования токена длиной {len(token) if token else 0}")
|
||||||
r = None
|
|
||||||
payload = None
|
if not token:
|
||||||
|
logger.error("[JWTCodec.decode] Пустой токен")
|
||||||
|
return None
|
||||||
|
|
||||||
try:
|
try:
|
||||||
payload = jwt.decode(
|
payload = jwt.decode(
|
||||||
token,
|
token,
|
||||||
|
@ -87,25 +90,29 @@ class JWTCodec:
|
||||||
# Добавим exp по умолчанию, чтобы избежать ошибки при создании TokenPayload
|
# Добавим exp по умолчанию, чтобы избежать ошибки при создании TokenPayload
|
||||||
payload["exp"] = int((datetime.now(tz=timezone.utc) + timedelta(days=30)).timestamp())
|
payload["exp"] = int((datetime.now(tz=timezone.utc) + timedelta(days=30)).timestamp())
|
||||||
|
|
||||||
r = TokenPayload(**payload)
|
try:
|
||||||
logger.debug(f"[JWTCodec.decode] Создан объект TokenPayload: user_id={r.user_id}, username={r.username}")
|
r = TokenPayload(**payload)
|
||||||
|
logger.debug(f"[JWTCodec.decode] Создан объект TokenPayload: user_id={r.user_id}, username={r.username}")
|
||||||
return r
|
return r
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"[JWTCodec.decode] Ошибка при создании TokenPayload: {e}")
|
||||||
|
return None
|
||||||
|
|
||||||
except jwt.InvalidIssuedAtError:
|
except jwt.InvalidIssuedAtError:
|
||||||
logger.error(f"[JWTCodec.decode] Недействительное время выпуска токена: {payload}")
|
logger.error("[JWTCodec.decode] Недействительное время выпуска токена")
|
||||||
raise ExpiredToken("jwt check token issued time")
|
return None
|
||||||
except jwt.ExpiredSignatureError:
|
except jwt.ExpiredSignatureError:
|
||||||
logger.error(f"[JWTCodec.decode] Истек срок действия токена: {payload}")
|
logger.error("[JWTCodec.decode] Истек срок действия токена")
|
||||||
raise ExpiredToken("jwt check token lifetime")
|
return None
|
||||||
except jwt.InvalidSignatureError:
|
except jwt.InvalidSignatureError:
|
||||||
logger.error("[JWTCodec.decode] Недействительная подпись токена")
|
logger.error("[JWTCodec.decode] Недействительная подпись токена")
|
||||||
raise InvalidToken("jwt check signature is not valid")
|
return None
|
||||||
except jwt.InvalidTokenError:
|
except jwt.InvalidTokenError:
|
||||||
logger.error("[JWTCodec.decode] Недействительный токен")
|
logger.error("[JWTCodec.decode] Недействительный токен")
|
||||||
raise InvalidToken("jwt check token is not valid")
|
return None
|
||||||
except jwt.InvalidKeyError:
|
except jwt.InvalidKeyError:
|
||||||
logger.error("[JWTCodec.decode] Недействительный ключ")
|
logger.error("[JWTCodec.decode] Недействительный ключ")
|
||||||
raise InvalidToken("jwt check key is not valid")
|
return None
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"[JWTCodec.decode] Неожиданная ошибка при декодировании: {e}")
|
logger.error(f"[JWTCodec.decode] Неожиданная ошибка при декодировании: {e}")
|
||||||
raise InvalidToken(f"Ошибка декодирования: {str(e)}")
|
return None
|
||||||
|
|
|
@ -254,9 +254,12 @@ class Author(Base):
|
||||||
# Получаем все атрибуты объекта
|
# Получаем все атрибуты объекта
|
||||||
result = {c.name: getattr(self, c.name) for c in self.__table__.columns}
|
result = {c.name: getattr(self, c.name) for c in self.__table__.columns}
|
||||||
|
|
||||||
# Добавляем роли, если они есть
|
# Добавляем роли как список идентификаторов и названий
|
||||||
if hasattr(self, 'roles') and self.roles:
|
if hasattr(self, 'roles'):
|
||||||
result['roles'] = [role.id for role in self.roles]
|
result['roles'] = []
|
||||||
|
for role in self.roles:
|
||||||
|
if isinstance(role, dict):
|
||||||
|
result['roles'].append(role.get('id'))
|
||||||
|
|
||||||
# скрываем защищенные поля
|
# скрываем защищенные поля
|
||||||
if not access:
|
if not access:
|
||||||
|
|
|
@ -118,9 +118,18 @@ class SessionManager:
|
||||||
Returns:
|
Returns:
|
||||||
Optional[TokenPayload]: Данные токена или None, если сессия недействительна
|
Optional[TokenPayload]: Данные токена или None, если сессия недействительна
|
||||||
"""
|
"""
|
||||||
|
logger.debug(f"[SessionManager.verify_session] Проверка сессии для токена: {token[:20]}...")
|
||||||
|
|
||||||
# Декодируем токен для получения payload
|
# Декодируем токен для получения payload
|
||||||
payload = JWTCodec.decode(token)
|
try:
|
||||||
if not payload:
|
payload = JWTCodec.decode(token)
|
||||||
|
if not payload:
|
||||||
|
logger.error("[SessionManager.verify_session] Не удалось декодировать токен")
|
||||||
|
return None
|
||||||
|
|
||||||
|
logger.debug(f"[SessionManager.verify_session] Успешно декодирован токен, user_id={payload.user_id}")
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"[SessionManager.verify_session] Ошибка при декодировании токена: {str(e)}")
|
||||||
return None
|
return None
|
||||||
|
|
||||||
# Получаем данные из payload
|
# Получаем данные из payload
|
||||||
|
@ -162,10 +171,34 @@ class SessionManager:
|
||||||
keys = await redis.keys("session:*")
|
keys = await redis.keys("session:*")
|
||||||
logger.debug(f"[SessionManager.verify_session] Все ключи сессий в Redis: {keys}")
|
logger.debug(f"[SessionManager.verify_session] Все ключи сессий в Redis: {keys}")
|
||||||
|
|
||||||
|
# Проверяем, можно ли доверять токену напрямую
|
||||||
|
# Если токен валидный и не истек, мы можем доверять ему даже без записи в Redis
|
||||||
|
if payload and payload.exp and payload.exp > datetime.now(tz=timezone.utc):
|
||||||
|
logger.info(f"[SessionManager.verify_session] Токен валиден по JWT, создаем сессию для {user_id}")
|
||||||
|
|
||||||
|
# Создаем сессию на основе валидного токена
|
||||||
|
session_data = {
|
||||||
|
"user_id": user_id,
|
||||||
|
"username": payload.username,
|
||||||
|
"created_at": datetime.now(tz=timezone.utc).isoformat(),
|
||||||
|
"expires_at": payload.exp.isoformat() if isinstance(payload.exp, datetime) else datetime.fromtimestamp(payload.exp, tz=timezone.utc).isoformat(),
|
||||||
|
}
|
||||||
|
|
||||||
|
# Сохраняем сессию в Redis
|
||||||
|
pipeline = redis.pipeline()
|
||||||
|
pipeline.hset(session_key, mapping=session_data)
|
||||||
|
pipeline.expire(session_key, 30 * 24 * 60 * 60)
|
||||||
|
pipeline.sadd(cls._make_user_sessions_key(user_id), token)
|
||||||
|
await pipeline.execute()
|
||||||
|
|
||||||
|
logger.info(f"[SessionManager.verify_session] Создана новая сессия для валидного токена: {session_key}")
|
||||||
|
return payload
|
||||||
|
|
||||||
# Если сессии нет, возвращаем None
|
# Если сессии нет, возвращаем None
|
||||||
return None
|
return None
|
||||||
|
|
||||||
# Если сессия найдена, возвращаем payload
|
# Если сессия найдена, возвращаем payload
|
||||||
|
logger.debug(f"[SessionManager.verify_session] Сессия найдена для пользователя {user_id}")
|
||||||
return payload
|
return payload
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
|
|
42
docs/auth.md
42
docs/auth.md
|
@ -101,6 +101,48 @@
|
||||||
- Время блокировки: 30 минут
|
- Время блокировки: 30 минут
|
||||||
- Сброс счетчика после успешного входа
|
- Сброс счетчика после успешного входа
|
||||||
|
|
||||||
|
## Обработка заголовков авторизации
|
||||||
|
|
||||||
|
### Особенности работы с заголовками в Starlette
|
||||||
|
|
||||||
|
При работе с заголовками в Starlette/FastAPI необходимо учитывать следующие особенности:
|
||||||
|
|
||||||
|
1. **Регистр заголовков**: Заголовки в объекте `Request` чувствительны к регистру. Для надежного получения заголовка `Authorization` следует использовать регистронезависимый поиск.
|
||||||
|
|
||||||
|
2. **Формат Bearer токена**: Токен может приходить как с префиксом `Bearer `, так и без него. Необходимо обрабатывать оба варианта.
|
||||||
|
|
||||||
|
### Правильное получение заголовка авторизации
|
||||||
|
|
||||||
|
```python
|
||||||
|
# Получение заголовка с учетом регистра
|
||||||
|
headers_dict = dict(req.headers.items())
|
||||||
|
token = None
|
||||||
|
|
||||||
|
# Ищем заголовок независимо от регистра
|
||||||
|
for header_name, header_value in headers_dict.items():
|
||||||
|
if header_name.lower() == SESSION_TOKEN_HEADER.lower():
|
||||||
|
token = header_value
|
||||||
|
break
|
||||||
|
|
||||||
|
# Обработка Bearer префикса
|
||||||
|
if token and token.startswith("Bearer "):
|
||||||
|
token = token.split("Bearer ")[1].strip()
|
||||||
|
```
|
||||||
|
|
||||||
|
### Распространенные проблемы и их решения
|
||||||
|
|
||||||
|
1. **Проблема**: Заголовок не находится при прямом обращении `req.headers.get("Authorization")`
|
||||||
|
**Решение**: Использовать регистронезависимый поиск по всем заголовкам
|
||||||
|
|
||||||
|
2. **Проблема**: Токен приходит с префиксом "Bearer" в одних запросах и без него в других
|
||||||
|
**Решение**: Всегда проверять и обрабатывать оба варианта
|
||||||
|
|
||||||
|
3. **Проблема**: Токен декодируется, но сессия не находится в Redis
|
||||||
|
**Решение**: Проверить формирование ключа сессии и добавить автоматическое создание сессии для валидных токенов
|
||||||
|
|
||||||
|
4. **Проблема**: Ошибки при декодировании JWT вызывают исключения
|
||||||
|
**Решение**: Обернуть декодирование в try-except и возвращать None вместо вызова исключений
|
||||||
|
|
||||||
## Конфигурация
|
## Конфигурация
|
||||||
|
|
||||||
Основные настройки в settings.py:
|
Основные настройки в settings.py:
|
||||||
|
|
Loading…
Reference in New Issue
Block a user