Безопасность и аутентификация FastAPI

FastAPI имеет встроенные инструменты безопасности, которые поддерживают распространённые методы аутентификации и авторизации, такие как OAuth2, JWT-токены и API-ключи. В этом разделе рассказывается, как реализовать аутентификацию пользователей на основе OAuth2 + JWT.


Обзор безопасности и аутентификации

Схемы безопасности, поддерживаемые FastAPI:

РешениеПрименимые сценарииОписание
HTTP Basic AuthПростой внутренний сервисИмя пользователя и пароль кодируются в заголовке запроса, что обеспечивает более низкую безопасность
Ключ APIМежсервисный вызовПередавать ключ через заголовки запроса, параметры запроса или cookie
OAuth2 + JWTПриложение с разделённым фронтендом и бэкендомСамый распространённый подход, безопасный и гибкий

Режим пароля OAuth2 + JWT

Это наиболее часто используемая схема аутентификации для приложений с разделёнными фронтендом и бэкендом. Поток:

  1. Клиент отправляет имя пользователя и пароль на/tokenКонечная точка
  2. Сервер проверяет учётные данные и возвращает токен доступа JWT
  3. Клиент передаёт токен в последующих запросах (Authorization: Bearer <token>)
  4. Сервер проверяет токен, чтобы идентифицировать пользователя

1. Установка зависимостей

pip install "python-jose[cryptography]" passlib[bcrypt]

2. Полный пример

Пример

from datetime import datetime, timedelta
from typing import Annotated

from fastapi import Depends, FastAPI, HTTPException, status
from fastapi.security import OAuth2PasswordBearer, OAuth2PasswordRequestForm
from jose import JWTError, jwt
from passlib.context import CryptContext
from pydantic import BaseModel

# ===== Конфигурация =====
SECRET_KEY = "your-secret-key-keep-it-secret"  # Используйте переменные окружения в продакшене
ALGORITHM = "HS256"
ACCESS_TOKEN_EXPIRE_MINUTES = 30

# ===== Хеширование паролей =====
pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")

# ===== Схема OAuth2 =====
oauth2_scheme = OAuth2PasswordBearer(tokenUrl="token")

# ===== Модели данных =====
class Token(BaseModel):
    access_token: str
    token_type: str


class TokenData(BaseModel):
    username: str | None = None


class User(BaseModel):
    username: str
    email: str | None = None
    full_name: str | None = None
    disabled: bool | None = None


class UserInDB(User):
    hashed_password: str


# ===== Фиктивная база данных =====
fake_users_db = {
    "alice": {
        "username": "alice",
        "full_name": "Алиса Уондерсон",
        "email": "[email protected]",
        "hashed_password": pwd_context.hash("secret"),  # Пароль: secret
        "disabled": False,
    }
}


# ===== Вспомогательные функции =====
def verify_password(plain_password: str, hashed_password: str) -> bool:
    """Проверка пароля"""
    return pwd_context.verify(plain_password, hashed_password)


def get_password_hash(password: str) -> str:
    """Генерация хеша пароля"""
    return pwd_context.hash(password)


def get_user(db: dict, username: str) -> UserInDB | None:
    """Получить пользователя из базы данных"""
    if username in db:
        return UserInDB(**db[username])
    return None


def authenticate_user(db: dict, username: str, password: str):
    """Проверить учетные данные пользователя"""
    user = get_user(db, username)
    if not user:
        return False
    if not verify_password(password, user.hashed_password):
        return False
    return user


def create_access_token(data: dict, expires_delta: timedelta | None = Отсутствует):
    """Создать JWT-токен доступа"""
    to_encode = data.copy()
    expire = datetime.utcnow() + (expires_delta or timedelta(minutes=15))
    to_encode.update({"exp": expire})
    return jwt.encode(to_encode, SECRET_KEY, algorithm=ALGORITHM)


async def get_current_user(token: Annotated[str, Depends(oauth2_scheme)]):
    """Получить текущего пользователя из токена (функция зависимости)"""
    credentials_exception = HTTPException(
        status_code=status.HTTP_401_UNAUTHORIZED,
        detail=Не удалось проверить учетные данные,
        headers={"WWW-Authenticate": "Bearer"},
    )
    try:
        payload = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM])
        username: str = payload.get("sub")
        if username is None:
            raise credentials_exception
    except JWTError:
        raise credentials_exception

    user = get_user(fake_users_db, username)
    if user is None:
        raise credentials_exception
    return user


async def get_current_active_user(
    current_user: Annotated[User, Depends(get_current_user)],
):
    """Получить текущего активного пользователя"""
    if current_user.disabled:
        raise HTTPException(status_code=400, detail=Пользователь был отключён)
    return current_user


# ===== Маршруты =====
app = FastAPI()


@app.post("/token")
async def login(form_data: Annotated[OAuth2PasswordRequestForm, Depends()]):
    """Вход для получения токена"""
    user = authenticate_user(fake_users_db, form_data.username, form_data.пароль)
    if not user:
        raise HTTPException(
            status_code=status.HTTP_401_UNAUTHORIZED,
            detail="Неверное имя пользователя или пароль",
            headers={"WWW-Authenticate": "Bearer"},
        )
    access_token_expires = timedelta(minutes=ACCESS_TOKEN_EXPIRE_MINUTES)
    access_token = create_access_token(
        data={"sub": user.username}, expires_delta=access_token_expires
    )
    return {"access_token": access_token, "token_type": "bearer"}


@app.get("/users/me")
async def read_users_me(
    current_user: Annotated[User, Depends(get_current_active_user)],
):
    """Получить информацию о текущем пользователе (требуется аутентификация)"""
    return current_user


@app.get("/users/me/items")
async def read_own_items(
    current_user: Annotated[User, Depends(get_current_active_user)],
):
    """Получить элементы текущего пользователя (требуется аутентификация)"""
    return [{"item_id": "Foo", "owner": current_user.username}]

Пояснение кода

Хеш пароля

ИспользованиеpasslibАлгоритм bcrypt хеширует пароли, чтобы гарантировать, что пароли в открытом виде не хранятся в базе данных:

ФункцияОписание
pwd_context.hash(password)Преобразовать пароль в открытом виде в хеш-значение
pwd_context.verify(plain, hashed)Проверить, соответствует ли пароль в открытом виде хеш-значению

JWT-токен

JWT (JSON Web Token) — это безопасный формат токена, который содержит информацию о пользователе и время истечения срока действия:

ДействиеФункцияОписание
Создать токенjwt.encode()Закодировать данные в строку JWT
Разобрать токенjwt.decode()Разобрать и проверить строку JWT
Установить срок действия"exp": expireВремя истечения срока действия токена
Сохранить идентификатор пользователя"sub": usernameСубъект токена (обычно имя пользователя)

OAuth2PasswordBearer

Сообщить FastAPI, откудаAuthorization: Bearer <token>Получить токен из заголовка запроса:

oauth2_scheme = OAuth2PasswordBearer(tokenUrl="token")

tokenUrl="token"Указывает путь конечной точки, по которому клиент получает токен; он будет отображаться в документации API.

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


Тестирование с помощью документации API

После настройки OAuth2 в Swagger UI отобразится кнопка "Authorize":

  1. Нажмите"Authorize"
  2. Введите имя пользователя и пароль (например,alice / secret)
  3. Нажмите"Authorize"Получить токен
  4. Все последующие запросы будут автоматически содержать токен

Итог

  • OAuth2 + JWT — наиболее часто используемая схема аутентификации для приложений с разделёнными фронтендом и бэкендом
  • Пароли должны хешироваться перед сохранением и не должны храниться в открытом виде
  • JWT-токены содержат идентификаторы пользователей и время истечения срока действия
  • Использование внедрения зависимостей (Depends) для реализации повторного использования логики аутентификации
  • В производственной среде SECRET_KEY должен храниться в секрете
Другие расширения