Как использовать HTTP Basic Auth в качестве отдельной службы FastAPI?

#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-адрес был добавлен в качестве переменной среды.