Безопасность и аутентификация FastAPI
FastAPI имеет встроенные инструменты безопасности, которые поддерживают распространённые методы аутентификации и авторизации, такие как OAuth2, JWT-токены и API-ключи. В этом разделе рассказывается, как реализовать аутентификацию пользователей на основе OAuth2 + JWT.
Обзор безопасности и аутентификации
Схемы безопасности, поддерживаемые FastAPI:
| Решение | Применимые сценарии | Описание |
|---|---|---|
| HTTP Basic Auth | Простой внутренний сервис | Имя пользователя и пароль кодируются в заголовке запроса, что обеспечивает более низкую безопасность |
| Ключ API | Межсервисный вызов | Передавать ключ через заголовки запроса, параметры запроса или cookie |
| OAuth2 + JWT | Приложение с разделённым фронтендом и бэкендом | Самый распространённый подход, безопасный и гибкий |
Режим пароля OAuth2 + JWT
Это наиболее часто используемая схема аутентификации для приложений с разделёнными фронтендом и бэкендом. Поток:
- Клиент отправляет имя пользователя и пароль на
/tokenКонечная точка - Сервер проверяет учётные данные и возвращает токен доступа JWT
- Клиент передаёт токен в последующих запросах (
Authorization: Bearer <token>) - Сервер проверяет токен, чтобы идентифицировать пользователя
1. Установка зависимостей
pip install "python-jose[cryptography]" passlib[bcrypt]
2. Полный пример
Пример
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":
- Нажмите"Authorize"
- Введите имя пользователя и пароль (например,
alice/secret) - Нажмите"Authorize"Получить токен
- Все последующие запросы будут автоматически содержать токен
Итог
- OAuth2 + JWT — наиболее часто используемая схема аутентификации для приложений с разделёнными фронтендом и бэкендом
- Пароли должны хешироваться перед сохранением и не должны храниться в открытом виде
- JWT-токены содержат идентификаторы пользователей и время истечения срока действия
- Использование внедрения зависимостей (
Depends) для реализации повторного использования логики аутентификации - В производственной среде SECRET_KEY должен храниться в секрете