Здравствуйте, по причине того, что среди тэгов Хабра не присутствует тэга Starlette или FastAPI, я пометил вопрос тэгом Python.
Моя проблема заключается в том, что после входа в систему, созданному объекту класса Response некорректно устанавливаются cookie, вернее даже они не устанавливаются вовсе.
Мой менеджер аутентификации, написанный для того, чтобы не плодить код в представлениях (выделена проблемная часть)
class Authentication:
def __init__(
self,
*,
cookie_key: str,
cookie_domain: str,
cookie_httponly: bool,
cookie_max_age: int,
crud: ConsumersCRUD,
credentials_exception_class: Type[Exception],
secret: str,
algorithm: str
) -> None:
self.cookie_key = cookie_key
self.cookie_domain = cookie_domain
self.cookie_httponly = cookie_httponly
self.cookie_max_age = cookie_max_age
self.crud = crud
self.credentials_exception_class = credentials_exception_class
self.secret = secret
self.algorithm = algorithm
async def _open_session(self, response: Response, username: str) -> None:
cookie_data = self.__dict__.copy()
pattern = 'cookie_'
for parameter in cookie_data.copy():
if not parameter.startswith(pattern):
cookie_data.pop(parameter)
else:
parameter_without_prefix = parameter.removeprefix(pattern)
cookie_data[parameter_without_prefix] = cookie_data.pop(parameter)
cookie_data['value'] = _generate_token(
username, self.cookie_max_age, self.secret, self.algorithm
)
return response.set_cookie(**cookie_data)
async def authorize(self, response: Response, credentials: OAuth2PasswordRequestForm) -> None:
consumer = await self.crud.get('username', credentials.username)
password_match = False
if consumer:
password_match = _check_password(
credentials.password, consumer.get('password')
)
if not password_match:
raise self.credentials_exception_class('Incorrectly entered login or password.')
return await self._open_session(response, credentials.username)
Некоторым покажется странным тело метода _open_session. Так необходимо. Для того, чтобы Вам было проще воспринимать этот метод, представьте, что в нем просто передаются позиционные аргументы вот таким образом:
async def _open_session(self, response: Response, username: str) -> None:
token = _generate_token(username, self.cookie_max_age, self.secret, self.algorithm)
response.set_cookie(key=self.cookie_key, domain=self.cookie_domain,
value=token, max_age=self.cookie_max_age, httponly=self.cookie_httponly)
В методе authorize осуществляется банальная проверка на то, введено ли существующее в базе имя пользователя и в случае, если оно найдено, осуществляется сопоставление двух паролей.
Вот сопутствующие менеджеру функции:
_context = CryptContext(schemes=["bcrypt"], deprecated="auto")
def _check_password(password: str, encrypted_password: str) -> bool:
return _context.verify(password, encrypted_password)
def _generate_token(username: str, expires: int, secret: str, algorithm: str) -> str:
payload = {
'sub': username,
'exp': datetime.utcnow() + timedelta(seconds=expires)
}
return jwt.encode(payload, secret, algorithm).decode('utf-8')
Представления:
auth = fastapi.APIRouter()
auth_templates = Jinja2Templates(directory=settings.CONSUMERS_FRONTEND)
consumers = ConsumersCRUD(database, consumers)
auth_manager = Authentication(
**settings.AUTH_MANAGER_SETTINGS,
crud=consumers,
credentials_exception_class=CredentialsValidationException
)
async def _convenient_display_template(template: str, request: fastapi.Request, **context):
# The request parameter might be required only in the context,
# but since it is required for display, it is handed down separately.
context['request'] = request
return auth_templates.TemplateResponse(template, context)
@auth.get('/sign-in')
async def sign_in(request: fastapi.Request):
return await _convenient_display_template('sign-in.html', request=request)
@auth.post('/sign-in')
async def sign_in(
request: fastapi.Request,
credentials: OAuth2PasswordRequestForm = fastapi.Depends(OAuth2PasswordRequestForm)
):
try:
response = RedirectResponse(settings.REDIRECT_AFTER_SIGN_IN, status_code=303)
await auth_manager.authorize(response, credentials)
return response
except CredentialsValidationException as error_details:
return await _convenient_display_template(
'sign-in.html', request, error=error_details
)
Некоторые настройки:
REDIRECT_AFTER_SIGN_UP = "/sign-in"
REDIRECT_AFTER_SIGN_IN = "/"
COOKIE_KEY = 'Authorization'
COOKIE_DOMAIN = 'localhost'
COOKIE_HTTPONLY = True
ACCESS_TOKEN_EXPIRES = 1800
SECRET = 'sdksjkajkjaJKDJKjkdjks3984984mjkdjksjdk'
ALGORITHM = 'HS256'
AUTH_MANAGER_SETTINGS = {
'cookie_key': COOKIE_KEY,
'cookie_domain': COOKIE_DOMAIN,
'cookie_httponly': COOKIE_HTTPONLY,
'cookie_max_age': ACCESS_TOKEN_EXPIRES,
'secret': SECRET,
'algorithm': ALGORITHM
}
При установке метода __call__, который имеет следующий вид:
async def __call__(self, request: Request) -> str:
"""
"""
print(request.headers)
print(request.cookies)
cookie_header = request.cookies.get(self.cookie_key)
cookie_scheme, cookie_param = get_authorization_scheme_param(cookie_header)
print('Header:', cookie_header)
print('Scheme:', cookie_scheme)
print('Param:', cookie_param)
return 'dssd'
и при перенаправлении на главную страницу, представление которой выглядит так:
@app.get('/')
async def homepage(auth_key: str = Depends(auth_manager)):
pass
метод отображает следующее:
Headers({'host': '127.0.0.1:8000', 'connection': 'keep-alive', 'cache-control': 'max-age=0', 'upgrade-insecure-requests': '1', 'user-agent': '', 'accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,applic
ation/signed-exchange;v=b3;q=0.9', 'sec-fetch-site': 'same-origin', 'sec-fetch-mode': 'navigate', 'sec-fetch-user': '?1', 'sec-fetch-dest': 'document', 'referer': '127.0.0.1:8000/c
onsumers/sign-in', 'accept-encoding': 'gzip, deflate, br', 'accept-language': 'ru-RU,ru;q=0.9,en-US;q=0.8,en;q=0.7', 'cookie': 'pga4_session=aa308bd8-c10a-4d98-9bab-5656c6e9a405!7b+kPpPJ+
NpVfHuhW4sB3YtatMs='})
{'pga4_session': 'aa308bd8-c10a-4d98-9bab-5656c6e9a405!7b+kPpPJ+NpVfHuhW4sB3YtatMs='}
Header: None
Scheme:
Param:
То есть ни в хедерах, ни в кукисах не находит ту установленную в ответе сессию.
Некоторые жалуются на большой объем кода в вопросах, а некоторые жалуются на обратное. Я постарался не впихивать лишнего и вынести максимум необходимого. Помогите, пожалуйста, в решении этой проблемы.