← назад к разделу

PYTHON · FASTAPI Фаза 5. Фундамент - ядро FastAPI

Обязательно

Аутентификация и авторизация во FastAPI

OAuth2 · JWT · Guards

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

1. Сначала кто, затем что можно

1 / 5

Аутентификация подтверждает личность и при неуспехе даёт 401. Авторизация проверяет разрешение уже известного пользователя и при отказе даёт 403.

Identification"я user 42"Authenticationpassword / JWT401Authorizationrole / scope403

2. JWT приходит в Authorization: Bearer

2 / 5

После проверки пароля сервер подписывает sub и exp. OAuth2PasswordBearer достаёт токен из заголовка, jwt.decode проверяет подпись и срок.

POST /auth/tokenJWT: sub + expBearer tokenjwt.decodesignature · expiration · issuer · audience

3. Текущий пользователь - зависимость

3 / 5

get_current_user читает sub, загружает пользователя и возвращает доменный объект. Любой эндпоинт с CurrentUser защищён без повторения разбора токена.

Bearertokenjwt.decodesub=42users.findUser 42CurrentUserendpoint

4. Guard проверяет роль и scope

4 / 5

require_admin зависит от CurrentUser и возвращает 403 до обработчика. Scopes нужны при множестве гранулярных разрешений, для нескольких ролей достаточно guards.

CurrentUseris_admin=falsescopes=[products:read]require_admincheck role403 ForbiddenDELETE /productне вызван

5. Безопасность не живёт в теле запроса

5 / 5

is_admin из JSON не доказательство, секреты хранятся в окружении, пароли - в Argon2. Access живёт минуты, refresh - в HttpOnly cookie, между сервисами используют RS256 и JWKS.

PasswordArgon2 hashslow + saltAccess token5-15 minRS256 + JWKSRefresh tokenHttpOnly cookierotation

Почти каждый реальный сервис должен знать, кто к нему обращается, и решать — разрешить или нет. Во FastAPI для этого нет отдельной магической подсистемы. Аутентификация и авторизация строятся на том же механизме, что и всё остальное — на зависимостях. «Текущий пользователь» — это зависимость. «Требуется роль» — тоже зависимость.

Аутентификация и авторизация — в чём разница

Два слова, которые часто путают:

  • Аутентификация — подтверждение личности. Кто ты? Сервер проверяет токен или пароль и устанавливает, кто обращается.
  • Авторизация — проверка прав. Что тебе можно? Сервер проверяет, имеет ли этот пользователь доступ к конкретному ресурсу.

Сначала идёт аутентификация, потом — авторизация. Нельзя проверить права, не зная, кто перед тобой.

Третье слово, которое спрашивают рядом с этими двумя, идентификация: это заявление «я такой-то», ещё не подтверждённое. Логин в форме входа или username в теле запроса это идентификация; пароль, код из приложения или подпись токена, которые это подтверждают, это аутентификация; правило «менеджер видит только свои заказы» это авторизация. Разница видна по ошибкам: не удалось подтвердить личность, отвечаем 401, личность подтверждена, но права нет, отвечаем 403.

Как устроен типичный поток

Самая распространённая схема для API:

POST /auth/token форма username, password jwt.encode sub, exp, HS256 Authorization: Bearer на каждом запросе get_current_user jwt.decode → User require_admin 403, если не админ

Токен выдают один раз по паролю, дальше каждый запрос несёт его в заголовке, а зависимости проверяют подпись и права до входа в эндпоинт.

  1. Клиент отправляет логин и пароль на специальный эндпоинт (/auth/token).
  2. Сервер проверяет данные и возвращает JWT-токен — подписанную строку.
  3. При каждом запросе клиент передаёт этот токен в заголовке: Authorization: Bearer <токен>.
  4. Сервер проверяет подпись токена и извлекает из него данные о пользователе.

FastAPI понимает этот поток и помогает его реализовать.

OAuth2PasswordBearer — как FastAPI достаёт токен

FastAPI предоставляет готовый инструмент для извлечения токена из заголовка Authorization: Bearer ... — OAuth2PasswordBearer. Он сам по себе является зависимостью: достаёт строку токена и передаёт её дальше.

from fastapi.security import OAuth2PasswordBearer

oauth2_scheme = OAuth2PasswordBearer(tokenUrl="auth/token")

Параметр tokenUrl указывает адрес эндпоинта, где клиент получает токен. FastAPI использует это только для документации (Swagger будет знать, куда вести за токеном).

JWT — что это такое и как его создать

JWT (JSON Web Token) — это подписанная строка из трёх частей: заголовка, полезной нагрузки и подписи. В полезной нагрузке лежат данные о пользователе и срок жизни токена. Сервер проверяет подпись — если она верна, данным можно доверять.

Для работы с JWT берут библиотеку PyJWT:

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 (expiration) — когда токен истекает; jwt.decode проверит это автоматически.
  • Секрет берётся из настроек приложения, не из кода напрямую.

Текущий пользователь как зависимость

Проверка токена и загрузка пользователя оформляются в одну зависимость. Затем эту зависимость требуют эндпоинты — и они автоматически становятся защищёнными.

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", headers={"WWW-Authenticate": "Bearer"})
    user = await users.find(int(payload["sub"]))
    if user is None:
        raise HTTPException(status_code=401, detail="invalid token", headers={"WWW-Authenticate": "Bearer"})
    return user


CurrentUser = Annotated[User, Depends(get_current_user)]

jwt.decode проверяет подпись и срок жизни. Если токен неверный или просроченный — поднимается InvalidTokenError, который превращается в ответ 401. Заголовок WWW-Authenticate: Bearer в этом ответе — требование стандарта: по нему клиент понимает, какую схему входа от него ждут.

Теперь любой эндпоинт может потребовать 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 — FastAPI сам выстраивает цепочку: токен есть → пользователь найден → пользователь является администратором. Проверка прав происходит до входа в тело эндпоинта.

Scopes — гранулярные права в токене

Когда прав много и они разные у разных клиентов, вместо набора отдельных зависимостей используют scopes — права, зашитые прямо в токен. Например, токен может содержать products:read products:write, а эндпоинт явно требует products:write.

OAuth2PasswordBearer объявляет доступные scopes, эндпоинт указывает нужный через Security(...), а зависимость сверяет, что требуемый scope есть в токене. Для небольшого сервиса с несколькими ролями достаточно guard-зависимостей. Scopes оправданы, когда прав много и они выдаются гранулярно разным клиентам.

Что не делать

Несколько правил, нарушение которых сводит на нет всё остальное:

  • Не хранить секреты в коде. JWT-секрет и ключи — только в переменных окружения, через настройки приложения.
  • Не доверять телу запроса для авторизации. Поле is_admin: true в JSON-теле — не доказательство. Данные для авторизации должны исходить от сервера (из токена или базы данных).
  • Проверять права на сервере. Скрыть кнопку на клиенте — не защита. Сервер всегда проверяет сам.
  • Давать токену конечный срок жизни. Бессрочный токен невозможно отозвать при утечке.
Дополнительно: при первом чтении можно пропустить

Глубже: выдача токена, хеширование пароля и refreshрасширенное

Эндпоинт, который выдаёт токен, принимает не JSON, а форму username/password: так устроен password flow OAuth2, и Swagger UI умеет с ним работать из коробки. FastAPI даёт готовую модель формы:

from fastapi.security import OAuth2PasswordRequestForm

@router.post("/auth/token")
async def issue_token(form: Annotated[OAuth2PasswordRequestForm, Depends()], users: UserRepositoryDep, settings: SettingsDep):
    user = await users.find_by_login(form.username)
    if user is None or not password_hash.verify(form.password, user.password_hash):
        raise HTTPException(status_code=401, detail="bad credentials", headers={"WWW-Authenticate": "Bearer"})
    return {"access_token": create_access_token(user.id, settings.jwt_secret.get_secret_value()), "token_type": "bearer"}

Пароли хранят только хешем с солью и медленной функцией. Долгое время для этого брали passlib, но он не развивается с 2020 года и ломается на новых версиях bcrypt; сейчас берут pwdlib с Argon2 (PasswordHash.recommended()) или argon2-cffi напрямую. Проверка пароля намеренно медленная, десятки миллисекунд: это защита от перебора по утёкшей базе.

Токен на час в примере выше — компромисс. В рабочих системах access-токен живёт 5–15 минут, а продлевает его refresh-токен в HttpOnly-cookie с ротацией; как это устроено, разобрано в статье о хранении токенов на клиенте.

HS256 с общим секретом годится, пока токен выпускает и проверяет один сервис. Как только проверяющих несколько, секрет пришлось бы раздать всем, и любой из них смог бы выпускать токены. Поэтому между сервисами используют RS256 или ES256: приватный ключ у того, кто выпускает, а проверяющие берут публичные ключи по JWKS; при проверке задают audience, issuer, небольшой leeway на расхождение часов и options={"require": ["exp", "sub"]}.

Коротко

  • Аутентификация — кто ты; авторизация — что тебе можно. Сначала первое, потом второе.
  • OAuth2PasswordBearer — зависимость, которая достаёт токен из заголовка Authorization: Bearer.
  • JWT — подписанная строка с данными о пользователе и сроком жизни. Подпись проверяется при каждом запросе.
  • Текущий пользователь — зависимость get_current_user; эндпоинт получает его через CurrentUser.
  • Авторизация — ещё одна зависимость (guard), проверяющая роль и бросающая 403.
  • Права в токене (scopes) удобны, когда клиентов много с разными разрешениями.
  • Секреты — только из окружения; is_admin из тела запроса — не доказательство.
  • Токен выдают через OAuth2PasswordRequestForm, пароли хешируют Argon2 через pwdlib, а не passlib; access живёт минуты, продлевает его refresh в HttpOnly-cookie; на несколько сервисов — RS256 и JWKS.

Что пощупать

Во взрослом каталоге практикума remodov/marketplace-system-python аутентификация и роли разведены по зависимостям FastAPI: optional_principal разбирает bearer-токен в Principal, require_roles говорит, кому можно вызывать ручку, а владение товаром проверяет не зависимость, а сценарий в ядре - у зависимости нет данных о товаре.

Код: auth.py, products.py, ownership.py.

Сделаем сами

Ветка step-07-adult-catalog - без токена 401, чужой товар 404; оба теста красные, пока смена цены не вернётся по слоям.

Что почитать дальше