Перейти к содержанию

Безопасность — первые шаги

Представим, что у вас есть бэкенд API на некотором домене.

И у вас есть фронтенд на другом домене или на другом пути того же домена (или в мобильном приложении).

И вы хотите, чтобы фронтенд мог аутентифицироваться на бэкенде, используя имя пользователя и пароль.

Мы можем использовать OAuth2, чтобы построить это с FastAPI.

Но давайте сэкономим вам время на чтение всей длинной спецификации в поисках тех небольших фрагментов информации, которые вам нужны.

Воспользуемся инструментами, предоставленными FastAPI, чтобы работать с безопасностью.

Как это выглядит

Сначала просто воспользуемся кодом и посмотрим, как он работает, а затем вернемся и разберемся, что происходит.

Создание main.py

Скопируйте пример в файл main.py:

from typing import Annotated

from fastapi import Depends, FastAPI
from fastapi.security import OAuth2PasswordBearer

app = FastAPI()

oauth2_scheme = OAuth2PasswordBearer(tokenUrl="token")


@app.get("/items/")
async def read_items(token: Annotated[str, Depends(oauth2_scheme)]):
    return {"token": token}
🤓 Other versions and variants
from fastapi import Depends, FastAPI
from fastapi.security import OAuth2PasswordBearer
from typing_extensions import Annotated

app = FastAPI()

oauth2_scheme = OAuth2PasswordBearer(tokenUrl="token")


@app.get("/items/")
async def read_items(token: Annotated[str, Depends(oauth2_scheme)]):
    return {"token": token}

Tip

Prefer to use the Annotated version if possible.

from fastapi import Depends, FastAPI
from fastapi.security import OAuth2PasswordBearer

app = FastAPI()

oauth2_scheme = OAuth2PasswordBearer(tokenUrl="token")


@app.get("/items/")
async def read_items(token: str = Depends(oauth2_scheme)):
    return {"token": token}

Запуск

Дополнительная информация

Пакет python-multipart автоматически устанавливается вместе с FastAPI, если вы запускаете команду pip install "fastapi[standard]".

Однако, если вы используете команду pip install fastapi, пакет python-multipart по умолчанию не включается.

Чтобы установить его вручную, убедитесь, что вы создали виртуальное окружение, активировали его и затем установили пакет:

$ pip install python-multipart

Это связано с тем, что OAuth2 использует «данные формы» для отправки username и password.

Запустите пример командой:

$ fastapi dev main.py

<span style="color: green;">INFO</span>:     Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit)

Проверка

Перейдите к интерактивной документации по адресу: http://127.0.0.1:8000/docs.

Вы увидите примерно следующее:

Кнопка авторизации!

У вас уже появилась новая кнопка «Authorize».

А у вашей операции пути в правом верхнем углу есть маленький замочек, на который можно нажать.

Если нажать на нее, появится небольшая форма авторизации, в которую нужно ввести username и password (и другие необязательные поля):

Примечание

Неважно, что вы введете в форму — пока это не будет работать. Но мы скоро до этого дойдем.

Конечно, это не фронтенд для конечных пользователей, но это отличный автоматический инструмент для интерактивного документирования всего вашего API.

Им может пользоваться команда фронтенда (которой можете быть и вы сами).

Им могут пользоваться сторонние приложения и системы.

И им также можете пользоваться вы сами, чтобы отлаживать, проверять и тестировать то же самое приложение.

«password flow» (аутентификация по паролю)

Теперь давайте немного вернемся и разберемся, что это все такое.

«password flow» — это один из способов («flows»), определенных в OAuth2, для обеспечения безопасности и аутентификации.

OAuth2 был спроектирован так, чтобы бэкенд или API были независимы от сервера, который аутентифицирует пользователя.

Но в нашем случае одно и то же приложение FastAPI будет работать и с API, и с аутентификацией.

Итак, рассмотрим это с упрощенной точки зрения:

  • Пользователь вводит на фронтенде username и password и нажимает Enter.
  • Фронтенд (работающий в браузере пользователя) отправляет эти username и password на конкретный URL в нашем API (объявленный с tokenUrl="token").
  • API проверяет этот username и password и отвечает «токеном» (мы еще ничего из этого не реализовали).
    • «Токен» — это просто строка с некоторым содержимым, которое мы сможем позже использовать для проверки этого пользователя.
    • Обычно у токена установлен срок действия: он истекает через некоторое время.
      • Поэтому пользователю придется снова войти в систему в какой‑то момент.
      • И если токен украдут, риск меньше: это не постоянный ключ, который будет работать вечно (в большинстве случаев).
  • Фронтенд временно где‑то хранит этот токен.
  • Пользователь кликает во фронтенде, чтобы перейти в другой раздел веб‑приложения.
  • Фронтенду нужно получить дополнительные данные из API.
    • Но для этого для конкретной конечной точки нужна аутентификация.
    • Поэтому, чтобы аутентифицироваться в нашем API, он отправляет HTTP-заголовок Authorization со значением Bearer плюс сам токен.
    • Если токен содержит foobar, то содержимое заголовка Authorization будет: Bearer foobar.

Класс OAuth2PasswordBearer в FastAPI

FastAPI предоставляет несколько средств на разных уровнях абстракции для реализации этих функций безопасности.

В этом примере мы будем использовать OAuth2, с потоком Password, используя токен Bearer. Для этого мы используем класс OAuth2PasswordBearer.

Дополнительная информация

Токен «bearer» — не единственный вариант.

Но для нашего случая он — лучший.

И он может быть лучшим для большинства случаев использования, если только вы не являетесь экспертом по OAuth2 и точно знаете, почему другой вариант лучше подходит под ваши нужды.

В этом случае FastAPI также предоставляет инструменты, чтобы его реализовать.

При создании экземпляра класса OAuth2PasswordBearer мы передаем параметр tokenUrl. Этот параметр содержит URL, который клиент (фронтенд, работающий в браузере пользователя) будет использовать для отправки username и password, чтобы получить токен.

from typing import Annotated

from fastapi import Depends, FastAPI
from fastapi.security import OAuth2PasswordBearer

app = FastAPI()

oauth2_scheme = OAuth2PasswordBearer(tokenUrl="token")


@app.get("/items/")
async def read_items(token: Annotated[str, Depends(oauth2_scheme)]):
    return {"token": token}
🤓 Other versions and variants
from fastapi import Depends, FastAPI
from fastapi.security import OAuth2PasswordBearer
from typing_extensions import Annotated

app = FastAPI()

oauth2_scheme = OAuth2PasswordBearer(tokenUrl="token")


@app.get("/items/")
async def read_items(token: Annotated[str, Depends(oauth2_scheme)]):
    return {"token": token}

Tip

Prefer to use the Annotated version if possible.

from fastapi import Depends, FastAPI
from fastapi.security import OAuth2PasswordBearer

app = FastAPI()

oauth2_scheme = OAuth2PasswordBearer(tokenUrl="token")


@app.get("/items/")
async def read_items(token: str = Depends(oauth2_scheme)):
    return {"token": token}

Подсказка

Здесь tokenUrl="token" ссылается на относительный URL token, который мы еще не создали. Поскольку это относительный URL, он эквивалентен ./token.

Поскольку мы используем относительный URL, если ваш API расположен по адресу https://example.com/, то он будет ссылаться на https://example.com/token. А если ваш API расположен по адресу https://example.com/api/v1/, то он будет ссылаться на https://example.com/api/v1/token.

Использование относительного URL важно для того, чтобы ваше приложение продолжало работать даже в таком продвинутом случае, как За прокси-сервером.

Этот параметр не создает конечную точку / операцию пути, а объявляет, что URL /token — это тот, который клиент должен использовать для получения токена. Эта информация используется в OpenAPI, а затем в интерактивных системах документации по API.

Скоро мы также создадим и саму операцию пути.

Дополнительная информация

Если вы очень строгий «питонист», вам может не понравиться стиль имени параметра tokenUrl вместо token_url.

Это потому, что используется то же имя, что и в спецификации OpenAPI. Так, если вам нужно разобраться подробнее в какой‑то из этих схем безопасности, вы можете просто скопировать и вставить это имя, чтобы найти больше информации.

Переменная oauth2_scheme — это экземпляр OAuth2PasswordBearer, но она также «вызываемая».

Ее можно вызвать так:

oauth2_scheme(some, parameters)

Поэтому ее можно использовать вместе с Depends.

Использование

Теперь вы можете передать oauth2_scheme как зависимость с Depends.

from typing import Annotated

from fastapi import Depends, FastAPI
from fastapi.security import OAuth2PasswordBearer

app = FastAPI()

oauth2_scheme = OAuth2PasswordBearer(tokenUrl="token")


@app.get("/items/")
async def read_items(token: Annotated[str, Depends(oauth2_scheme)]):
    return {"token": token}
🤓 Other versions and variants
from fastapi import Depends, FastAPI
from fastapi.security import OAuth2PasswordBearer
from typing_extensions import Annotated

app = FastAPI()

oauth2_scheme = OAuth2PasswordBearer(tokenUrl="token")


@app.get("/items/")
async def read_items(token: Annotated[str, Depends(oauth2_scheme)]):
    return {"token": token}

Tip

Prefer to use the Annotated version if possible.

from fastapi import Depends, FastAPI
from fastapi.security import OAuth2PasswordBearer

app = FastAPI()

oauth2_scheme = OAuth2PasswordBearer(tokenUrl="token")


@app.get("/items/")
async def read_items(token: str = Depends(oauth2_scheme)):
    return {"token": token}

Эта зависимость предоставит str, который будет присвоен параметру token функции-обработчика пути.

FastAPI будет знать, что может использовать эту зависимость для определения «схемы безопасности» в схеме OpenAPI (и в автоматической документации по API).

Технические детали

FastAPI будет знать, что может использовать класс OAuth2PasswordBearer (объявленный в зависимости) для определения схемы безопасности в OpenAPI, потому что он наследуется от fastapi.security.oauth2.OAuth2, который, в свою очередь, наследуется от fastapi.security.base.SecurityBase.

Все утилиты безопасности, интегрируемые с OpenAPI (и автоматической документацией по API), наследуются от SecurityBase, — так FastAPI понимает, как интегрировать их в OpenAPI.

Что он делает

Он будет искать в запросе заголовок Authorization, проверять, что его значение — это Bearer плюс некоторый токен, и вернет токен как str.

Если заголовок Authorization отсутствует или его значение не содержит токен Bearer, он сразу ответит ошибкой со статус-кодом 401 (UNAUTHORIZED).

Вам даже не нужно проверять наличие токена, чтобы вернуть ошибку. Вы можете быть уверены: если ваша функция была выполнена, в этом токене будет str.

Это уже можно попробовать в интерактивной документации:

Мы пока не проверяем валидность токена, но для начала это уже неплохо.

Резюме

Таким образом, всего за 3–4 дополнительные строки у вас уже есть некая примитивная форма защиты.