Аутентификация (кто ты) и авторизация (что тебе можно) во FastAPI строятся на том же механизме, что и всё остальное, — на зависимостях. «Текущий пользователь» — это зависимость; «требуется роль» — тоже зависимость. Это делает безопасность не отдельной подсистемой, а частью сборки эндпоинта, и её легко применять точечно и тестировать.
Извлечение токена: OAuth2PasswordBearer
FastAPI даёт готовый способ достать токен из заголовка Authorization: Bearer ... — OAuth2PasswordBearer. Это сама по себе зависимость: подставит токен в обработчик и опишет схему в документации.
from fastapi.security import OAuth2PasswordBearer
oauth2_scheme = OAuth2PasswordBearer(tokenUrl="auth/token")
tokenUrl указывает, где клиент получает токен (эндпоинт логина). Сам логин проверяет логин/пароль и выдаёт JWT — ниже про его сборку.
JWT
Токен — это JWT: подписанная строка с полезной нагрузкой (кто пользователь, когда истекает). Для работы с ним берут поддерживаемую библиотеку — PyJWT (import jwt).
from datetime import datetime, timedelta, timezone
import jwt
def create_access_token(user_id: int, secret: str) -> str:
payload = {
"sub": str(user_id),
"exp": datetime.now(timezone.utc) + timedelta(hours=1),
}
return jwt.encode(payload, secret, algorithm="HS256")
sub — кто (subject), exp — когда истекает (проверяется при декодировании автоматически). Секрет берётся из настроек, не из кода.
Текущий пользователь как зависимость
Проверка токена и загрузка пользователя — зависимость, которую затем требуют эндпоинты.
from typing import Annotated
from fastapi import Depends, HTTPException
async def get_current_user(
token: Annotated[str, Depends(oauth2_scheme)],
settings: SettingsDep,
users: UserRepositoryDep,
) -> User:
try:
payload = jwt.decode(token, settings.jwt_secret, algorithms=["HS256"])
except jwt.InvalidTokenError:
raise HTTPException(status_code=401, detail="invalid token")
user = await users.find(int(payload["sub"]))
if user is None:
raise HTTPException(status_code=401, detail="invalid token")
return user
CurrentUser = Annotated[User, Depends(get_current_user)]
jwt.decode проверит подпись и срок; неверный или просроченный токен бросит InvalidTokenError, который превращается в 401. Теперь любой эндпоинт, потребовавший CurrentUser, защищён — без единой строки про токены в самом эндпоинте.
@router.get("/me", response_model=UserResponse)
async def me(user: CurrentUser):
return UserResponse.from_domain(user)
Зависимости-как-guards
Авторизация — следующий слой: пользователь известен, но можно ли ему именно это. Это снова зависимость, проверяющая право и бросающая 403.
def require_admin(user: CurrentUser) -> User:
if not user.is_admin:
raise HTTPException(status_code=403, detail="forbidden")
return user
AdminUser = Annotated[User, Depends(require_admin)]
@router.delete("/{product_id}", status_code=204)
async def delete_product(product_id: int, admin: AdminUser):
...
require_admin зависит от get_current_user — дерево зависимостей собирает цепочку «есть токен → есть пользователь → пользователь админ». Право проверяется до входа в эндпоинт.
Scopes
Когда прав много, вместо россыпи guard-ов используют scopes — права, зашитые в токен и требуемые на эндпоинте. OAuth2PasswordBearer объявляет доступные scopes, эндпоинт требует нужный через Security(...), а зависимость сверяет, что в токене он есть. Это даёт декларативную модель «эндпоинт требует products:write» вместо ручных проверок в каждом guard-е. Для небольшого сервиса хватает guard-ов по ролям; scopes оправданы, когда прав много и они выдаются гранулярно.
Чего не делать
Несколько правил, нарушение которых сводит на нет остальное: не хранить секреты и ключи в коде (только в настройках из окружения); не доверять полям из тела запроса для авторизации (is_admin из JSON — не доказательство); проверять право на сервере, а не прятать кнопку на клиенте; давать токену конечный срок жизни.
Где это в UCP
Аутентификация и авторизация выражены зависимостями, поэтому они — часть контракта эндпоинта, а не сквозной магии: видно прямо в сигнатуре, что роут требует пользователя или админа. Бизнес-правила доступа (этот пользователь владеет этим заказом) живут в Handler-е и домене, а не в guard-е — guard проверяет роль, домен проверяет принадлежность. Это та же раскладка, что в Spring Security в Java-биндинге: каркас аутентификации — фреймворком, правила доступа — методологией. Явная, проверяемая безопасность — одна из вещей, которые продукт-инженер обязан держать сам.