#python #authentication #microservices #basic-authentication #fastapi
#python #аутентификация #микросервисы #базовая аутентификация #fastapi
Вопрос:
Чего я хочу добиться?Есть одна служба, отвечающая за базовую аутентификацию HTTP (доступ), и две службы (a, b), где некоторые конечные точки защищены службой доступа.
Почему?В сценарии, где будет гораздо больше служб с защищенными конечными точками, чтобы не дублировать функцию авторизации в каждой службе. Также для внесения изменений в одном месте в случае перехода на OAuth2 (возможно, в будущем).
Что я сделал?Я следовал руководству на официальном сайте и создал пример сервиса, который работает абсолютно нормально.
Проблема возникает, когда я пытаюсь перенести авторизацию в отдельную службу, а затем использовать ее в нескольких других службах с защищенными конечными точками. Я не могу понять, как это сделать. Не могли бы вы мне помочь?
Я пробовал настраивать разные функции. Ничего не помогло, пока мой код выглядит так:
access-service
import os
import secrets
from fastapi import FastAPI, Depends, HTTPException, status
from fastapi.security import HTTPBasic, HTTPBasicCredentials
security = HTTPBasic()
def authorize(credentials: HTTPBasicCredentials = Depends(security)):
is_user_ok = secrets.compare_digest(credentials.username, os.getenv('LOGIN'))
is_pass_ok = secrets.compare_digest(credentials.password, os.getenv('PASSWORD'))
if not (is_user_ok and is_pass_ok):
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail='Incorrect email or password.',
headers={'WWW-Authenticate': 'Basic'},
)
app = FastAPI(openapi_url="/api/access/openapi.json", docs_url="/api/access/docs")
@app.get('/api/access/auth', dependencies=[Depends(authorize)])
def auth():
return {"Granted": True}
a-service
import httpx
import os
from fastapi import Depends, FastAPI, HTTPException, status
ACCESS_SERVICE_URL = os.getenv('ACCESS_SERVICE_URL')
app = FastAPI(openapi_url="/api/a/openapi.json", docs_url="/api/a/docs")
def has_access():
result = httpx.get(os.getenv('ACCESS_SERVICE_URL'))
if result.status_code == 401:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail='No access to resource. Login first.',
)
@app.get('/api/a/unprotected_a')
async def unprotected_a():
return {"Protected": False}
@app.get('/api/a/protected_a', dependencies=[Depends(has_access)])
async def protected_a():
return {"Protected": True}
@app.get('/api/a/protected_b', dependencies=[Depends(has_access)])
async def protected_b():
return {"Protected": True}
Комментарии:
1. пожалуйста, проверьте мой ответ
Ответ №1:
Проблема здесь в том, что, когда вы вызываете Service_A с учетными данными, он вызывает Access_Service в функции has_access() .
Если вы посмотрите внимательно,
result = httpx.get(os.getenv('ACCESS_SERVICE_URL'))
Вы просто выполняете вызов GET без пересылки учетных данных в качестве заголовков для этого запроса в Access_Service.
Перепишите ваш has_access() во всех службах, чтобы
from typing import Optional
from fastapi import Header
def has_access(authorization: Optional[str] = Header(None)):
if not authorization:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail='No access to resource. Credentials missing!',
)
headers = {'Authorization': authorization}
result = httpx.get(os.getenv('ACCESS_SERVICE_URL'), headers=headers)
if result.status_code == 401:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail='No access to resource. Login first.',
)
Внутри вашей службы доступа вы ошибочно ввели True как true,
@app.get('/api/access/auth', dependencies=[Depends(authorize)])
def auth():
return {"Granted": True}
Я клонировал ваш репозиторий и протестировал его, теперь он работает. Пожалуйста, проверьте и подтвердите.
[ПРАВИТЬ] Swagger не разрешает заголовок авторизации для базовой аутентификации (https://github.com/tiangolo/fastapi/issues/612 )
Обходной путь (не рекомендуется)
from fastapi.security import HTTPBasic, HTTPBasicCredentials
security = HTTPBasic()
def has_access(credentials: HTTPBasicCredentials = Depends(security), authorization: Optional[str] = Header(None)):
Комментарии:
1. Спасибо, что взглянули на это. Я проверил ваш ответ, но я получаю
AttributeError: 'NoneType' object has no attribute 'encode'
. Вот обновленный репозиторий ,2. Откуда вы вызываете API, из браузера или Postman?
3. Я делаю это из браузера. Я использую пользовательский интерфейс Swagger в 0.0.0.0:8080/api/a/docs
4. @Ethr — Я обновил код, ошибка возникает из-за того, что при вызове Service_A вы не вводите учетные данные, я смог воспроизвести это!
5. @Ether — localhost:8080/api/a/docs#/default/ … — вам нужно поместить здесь строку авторизации, прежде чем нажать Execute.
Ответ №2:
Благодаря ответу Soumojit Ghosh и проблеме FastAPI 1037 я понял, как мне следует изменить свой код. a-сервис после изменений:
import httpx
import os
from fastapi import Depends, FastAPI, Header, HTTPException, status
from typing import Optional
from fastapi.security import HTTPBasicCredentials, HTTPBearer
security = HTTPBearer()
ACCESS_SERVICE_URL = os.getenv('ACCESS_SERVICE_URL')
app = FastAPI(openapi_url="/api/a/openapi.json", docs_url="/api/a/docs")
def has_access(credentials: HTTPBasicCredentials = Depends(security)):
response = httpx.get(os.getenv('ACCESS_SERVICE_URL'), headers={'Authorization': credentials.credentials})
if response.status_code == 401:
raise HTTPException(status_code=401)
@app.get('/api/a/unprotected_a')
async def unprotected_a():
return {"Protected": False}
@app.get('/api/a/protected_a', dependencies=[Depends(has_access)])
async def protected_a():
return {"Protected": True}
@app.get('/api/a/protected_b', dependencies=[Depends(has_access)])
async def protected_b():
return {"Protected": True}
Теперь заголовок можно отправлять через SwaggerUI. Нажмите кнопку Авторизовать, а затем введите его в поле Значение. Чтобы сгенерировать свой заголовок на основе логина и пароля, вы можете использовать, например, этот инструмент. Это будет выглядеть так: Basic YWRtaW46cGFzc3dvcmQ=
.
Комментарии:
1. @laplace его URL- адрес службы доступа — access_service:8000/api/access/auth . Приложение было создано с помощью docker-compose. Этот URL-адрес был добавлен в качестве переменной среды.