FastAPI с JWT аутентификацией

11 ноября 2026
Автор

Антон Ларичев

Что такое JWT и зачем он нужен

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

При аутентификации через JWT сервер не хранит состояние сессии. Клиент получает токен при входе и передаёт его в каждом запросе. Сервер проверяет подпись и извлекает данные пользователя из payload — без обращения к хранилищу сессий.

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

Курс по теме

Основы Python — курс

40 000+ студентов · рейтинг 4.8 · гарантия возврата 30 дней

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

Создайте виртуальное окружение и установите необходимые пакеты:

python -m venv venv
source venv/bin/activate  # Windows: venv\Scripts\activate
pip install fastapi uvicorn[standard] python-jose[cryptography] passlib[bcrypt] python-multipart

Описание пакетов:

  • fastapi — веб-фреймворк
  • uvicorn — ASGI-сервер
  • python-jose — работа с JWT
  • passlib — хеширование паролей
  • python-multipart — обработка форм (требуется для OAuth2PasswordRequestForm)

Структура проекта

project/
├── main.py
├── auth.py
├── models.py
└── database.py

Настройка конфигурации

Создайте файл auth.py с базовыми настройками и вспомогательными функциями:

from datetime import datetime, timedelta
from typing import Optional
from jose import JWTError, jwt
from passlib.context import CryptContext

SECRET_KEY = "your-secret-key-change-in-production"
ALGORITHM = "HS256"
ACCESS_TOKEN_EXPIRE_MINUTES = 30

pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")


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 create_access_token(data: dict, expires_delta: Optional[timedelta] = None) -> str:
    to_encode = data.copy()
    if expires_delta:
        expire = datetime.utcnow() + expires_delta
    else:
        expire = datetime.utcnow() + timedelta(minutes=15)
    to_encode.update({"exp": expire})
    return jwt.encode(to_encode, SECRET_KEY, algorithm=ALGORITHM)


def decode_token(token: str) -> dict:
    return jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM])

В продакшене SECRET_KEY должен генерироваться случайным образом и храниться в переменных окружения:

openssl rand -hex 32

Модели данных

Создайте файл models.py с Pydantic-схемами:

from pydantic import BaseModel, EmailStr
from typing import Optional


class UserBase(BaseModel):
    username: str
    email: EmailStr


class UserCreate(UserBase):
    password: str


class UserInDB(UserBase):
    hashed_password: str


class User(UserBase):
    id: int

    class Config:
        from_attributes = True


class Token(BaseModel):
    access_token: str
    token_type: str


class TokenData(BaseModel):
    username: Optional[str] = None

Хранилище пользователей

Для примера используем словарь в памяти. В реальном приложении замените на работу с базой данных через SQLAlchemy или другой ORM.

Создайте файл database.py:

from models import UserInDB

fake_users_db: dict[str, UserInDB] = {}
user_counter: int = 0


def get_user(username: str) -> UserInDB | None:
    return fake_users_db.get(username)


def create_user(username: str, email: str, hashed_password: str) -> UserInDB:
    global user_counter
    user_counter += 1
    user = UserInDB(
        username=username,
        email=email,
        hashed_password=hashed_password,
    )
    fake_users_db[username] = user
    return user

Зависимости FastAPI для аутентификации

В FastAPI зависимости (dependencies) позволяют переиспользовать логику проверки токена в любом эндпоинте. Добавьте в auth.py:

from fastapi import Depends, HTTPException, status
from fastapi.security import OAuth2PasswordBearer
from jose import JWTError
from models import TokenData, UserInDB
from database import get_user

oauth2_scheme = OAuth2PasswordBearer(tokenUrl="token")


async def get_current_user(token: str = Depends(oauth2_scheme)) -> UserInDB:
    credentials_exception = HTTPException(
        status_code=status.HTTP_401_UNAUTHORIZED,
        detail="Не удалось проверить учётные данные",
        headers={"WWW-Authenticate": "Bearer"},
    )
    try:
        payload = decode_token(token)
        username: str = payload.get("sub")
        if username is None:
            raise credentials_exception
        token_data = TokenData(username=username)
    except JWTError:
        raise credentials_exception

    user = get_user(token_data.username)
    if user is None:
        raise credentials_exception
    return user

OAuth2PasswordBearer автоматически извлекает токен из заголовка Authorization: Bearer <token> и добавляет кнопку авторизации в Swagger UI.

Основное приложение

Создайте файл main.py:

from datetime import timedelta
from fastapi import Depends, FastAPI, HTTPException, status
from fastapi.security import OAuth2PasswordRequestForm

from auth import (
    ACCESS_TOKEN_EXPIRE_MINUTES,
    create_access_token,
    get_current_user,
    get_password_hash,
    verify_password,
)
from database import create_user, get_user
from models import Token, User, UserCreate, UserInDB

app = FastAPI(title="FastAPI JWT Auth")


@app.post("/register", response_model=User, status_code=status.HTTP_201_CREATED)
async def register(user_data: UserCreate):
    if get_user(user_data.username):
        raise HTTPException(
            status_code=status.HTTP_400_BAD_REQUEST,
            detail="Пользователь с таким именем уже существует",
        )
    hashed_password = get_password_hash(user_data.password)
    user = create_user(user_data.username, user_data.email, hashed_password)
    return user


@app.post("/token", response_model=Token)
async def login(form_data: OAuth2PasswordRequestForm = Depends()):
    user = get_user(form_data.username)
    if not user or not verify_password(form_data.password, user.hashed_password):
        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 Token(access_token=access_token, token_type="bearer")


@app.get("/users/me", response_model=User)
async def read_users_me(current_user: UserInDB = Depends(get_current_user)):
    return current_user


@app.get("/protected")
async def protected_route(current_user: UserInDB = Depends(get_current_user)):
    return {"message": f"Привет, {current_user.username}! Это защищённый маршрут."}

Запуск и тестирование

Запустите сервер:

uvicorn main:app --reload

Откройте документацию по адресу http://localhost:8000/docs. Swagger UI автоматически добавит кнопку «Authorize», где можно ввести логин и пароль для получения токена.

Тестирование через curl

Регистрация нового пользователя:

curl -X POST "http://localhost:8000/register" \
  -H "Content-Type: application/json" \
  -d '{"username": "alice", "email": "alice@example.com", "password": "secret123"}'

Получение токена:

curl -X POST "http://localhost:8000/token" \
  -H "Content-Type: application/x-www-form-urlencoded" \
  -d "username=alice&password=secret123"

Запрос к защищённому эндпоинту:

curl "http://localhost:8000/protected" \
  -H "Authorization: Bearer <ваш_токен>"

Добавление Refresh Token

Access-токены намеренно делают короткоживущими. Refresh-токен позволяет получить новый access-токен без повторного ввода пароля.

Добавьте в auth.py:

REFRESH_TOKEN_EXPIRE_DAYS = 7


def create_refresh_token(data: dict) -> str:
    to_encode = data.copy()
    expire = datetime.utcnow() + timedelta(days=REFRESH_TOKEN_EXPIRE_DAYS)
    to_encode.update({"exp": expire, "type": "refresh"})
    return jwt.encode(to_encode, SECRET_KEY, algorithm=ALGORITHM)

Обновите модель токена в models.py:

class Token(BaseModel):
    access_token: str
    refresh_token: str
    token_type: str

Обновите эндпоинт /token и добавьте эндпоинт обновления в main.py:

from auth import create_refresh_token, decode_token


@app.post("/token", response_model=Token)
async def login(form_data: OAuth2PasswordRequestForm = Depends()):
    user = get_user(form_data.username)
    if not user or not verify_password(form_data.password, user.hashed_password):
        raise HTTPException(
            status_code=status.HTTP_401_UNAUTHORIZED,
            detail="Неверное имя пользователя или пароль",
            headers={"WWW-Authenticate": "Bearer"},
        )
    access_token = create_access_token(
        data={"sub": user.username},
        expires_delta=timedelta(minutes=ACCESS_TOKEN_EXPIRE_MINUTES),
    )
    refresh = create_refresh_token(data={"sub": user.username})
    return Token(
        access_token=access_token,
        refresh_token=refresh,
        token_type="bearer",
    )


@app.post("/token/refresh", response_model=Token)
async def refresh_access_token(refresh_token: str):
    credentials_exception = HTTPException(
        status_code=status.HTTP_401_UNAUTHORIZED,
        detail="Недействительный refresh-токен",
    )
    try:
        payload = decode_token(refresh_token)
        if payload.get("type") != "refresh":
            raise credentials_exception
        username: str = payload.get("sub")
        if username is None:
            raise credentials_exception
    except JWTError:
        raise credentials_exception

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

    new_access_token = create_access_token(data={"sub": username})
    new_refresh_token = create_refresh_token(data={"sub": username})
    return Token(
        access_token=new_access_token,
        refresh_token=new_refresh_token,
        token_type="bearer",
    )

Использование переменных окружения

Выносите секретные данные в .env-файл:

pip install python-dotenv

Создайте .env:

SECRET_KEY=super-secret-key-generated-with-openssl
ACCESS_TOKEN_EXPIRE_MINUTES=30
REFRESH_TOKEN_EXPIRE_DAYS=7

Обновите начало auth.py:

import os
from dotenv import load_dotenv

load_dotenv()

SECRET_KEY = os.getenv("SECRET_KEY", "fallback-secret-key")
ACCESS_TOKEN_EXPIRE_MINUTES = int(os.getenv("ACCESS_TOKEN_EXPIRE_MINUTES", 30))
REFRESH_TOKEN_EXPIRE_DAYS = int(os.getenv("REFRESH_TOKEN_EXPIRE_DAYS", 7))

Добавьте .env в .gitignore, чтобы не передавать секреты в репозиторий.

Типичные ошибки

Токен не принимается

Убедитесь, что клиент отправляет заголовок в формате Authorization: Bearer <token>, а не Authorization: Token <token> или без префикса.

JWTError: Signature verification failed

Происходит, если SECRET_KEY изменился после выдачи токена — например, при перезапуске без переменных окружения. Все ранее выданные токены становятся недействительными.

422 Unprocessable Entity на /token

Эндпоинт /token принимает данные в формате application/x-www-form-urlencoded, а не JSON. Передавайте данные как форму, используя OAuth2PasswordRequestForm, а не JSON-тело.

Истёкший токен возвращает 401

Это ожидаемое поведение. Клиент должен обработать статус 401, автоматически вызвать /token/refresh и повторить исходный запрос с новым access-токеном.


Чтобы глубже освоить Python и научиться создавать production-ready приложения, пройдите курс на PurpleSchool: https://purpleschool.ru/course/python?utm_source=knowledgebase&utm_medium=text&utm_campaign=fastapi-jwt-authentication

Стрелочка влевоFastAPI lifespan: управление ресурсами приложенияФоновые задачи FastAPI — BackgroundTasksСтрелочка вправо

Постройте личный план изучения Python до уровня Middle — бесплатно!

Python — часть карты развития Backend

  • step100+ шагов развития
  • lessons30 бесплатных лекций
  • lessons300 бонусных рублей на счет

Все гайды по Python

Как отправлять запросы с помощью requests в PythonПочему Python выводит значение без команды printКак работает команда print в PythonВозможности Python для автоматизации задачРабота с JSON в Python на примерахPython get — методы получения данныхКак находить и исправлять ошибки в PythonРабота с данными через API и внешние сервисыСтруктура и оформление кода PythonОсновы Django с PythonПолезные приёмы в Python для повседневной работыИспользование locals в Python для отладкиИнтеграция PHP и PythonКак выполнять HTTPS-запросы в PythonFastAPI Python — быстрый старт: создание REST API с нуляКак работать с API в Python
Ввод целого числа в PythonВедение логов в PythonОбработка исключений с помощью try/except в PythonУдаление данных в Python с помощью removeФункция super() в Python — как вызвать метод родителяСоздание собственных контекстных менеджеров в PythonРабота с символами программирования PythonРабота с переменной X в PythonКак скачать Python на компьютерОсновы Python для тех, кто начинаетПростая программа на Python для начинающихРабота с классами в PythonЧто нового в Python 3Поддерживается ли Python 2 и стоит ли его использоватьКоманда python print - полное руководство по выводу данныхПравила именования переменных в PythonПользовательские исключения в PythonОсновы Python coreОписание объектов PythonНаследование классов в Python — основы и примерыPython 1 — с чего начиналась история языкаМножественное наследование в Python — примеры и MROКонтекстный менеджер with в Python — как работает и зачем нуженКомментарии в Python — однострочные, многострочные и docstringКакой Python выбрать для установкиКак вывести целое число с помощью print в PythonКак установить Python на Windows macOS и LinuxКак пользоваться консолью PythonКак получить последний элемент в PythonКак найти значение в PythonКак настроить PythonКак использовать print для строк в PythonКак работает интерпретатор PythonИнструкция по работе с PythonБлок finally в обработке исключений PythonЦелые числа в PythonАбстрактные классы в Python — ABC и abstractmethod
uv — современный менеджер пакетов PythonPython tomllib — встроенная работа с TOML файламиPython subprocess: запуск внешних команд и процессовВеб-скрапинг с Scrapy в PythonSQLModel ORM для FastAPITyper: создание CLI приложений на PythonPython rich: красивый вывод в терминалеМиграция с Pydantic v1 на v2Pydantic: валидация данных в PythonPoetry: управление зависимостями в PythonPython pathlib — современная работа с файловыми путямиPython с OpenAI API: от установки до потоковых ответовPython msgspec: быстрая сериализация данныхLangChain: основы работы с библиотекойPython click: создание CLI-приложенийPython itertools — работа с итераторамиПарсинг сайтов с BeautifulSoup в PythonAlembic для миграций базы данных с SQLAlchemyPython argparse: разбор аргументов командной строки
Загрузка данных PythonУправление проектами на GitHub с PythonСоздание веб-приложений на Flask PythonСоздание бота на PythonСоздание интерфейсов Python QTСоздание игр с PygameСоздание GUI в PythonКак работать со словарями в PythonРабота с библиотеками через Python PackagingРабота со временем в Python при помощи модуля timePython name — особенности переменнойМатематические операции в Python с модулем mathPython listing — что это и как использоватьPytest — тестирование на Python: полное руководствоОбработка изображений с OpenCV PythonNumPy в Python — основы и применение в задачахМашинное обучение с PythonМодуль contextlib в Python — утилиты для контекстных менеджеровИспользование Anaconda с PythonБиблиотеки Python и их применение в проектах
Возврат значений из функции в PythonВложенные функции в PythonСоздание собственных декораторов в PythonРабота с функцией map в PythonЦикл while в Python и примеры использованияОбработка чисел, введённых через input в PythonОсновные операторы в Python с примерамиУсловные выражения if else в Python для начинающихПродвинутые генераторы в Python — send, throw, close и корутиныКак выполняется вызов функций call в PythonПозиционные и именованные аргументы в PythonОбъявление переменных и управление областью видимости в PythonПередача аргументов по ссылке и по значению в PythonПередача аргументов через args и kwargs в PythonОсновные методы Python и примеры их использованияОператор match/case в Python 3.10+ — основы структурного сопоставленияПаттерны match/case в Python — деструктуризация, guard и вложенные шаблоныПрактические примеры match/case в Python — реальные сценарии примененияЛокальные и глобальные переменные в PythonЧасто используемые команды PythonКлючевые слова global и nonlocal в PythonКак создавать функции в PythonКак работает сборщик мусора в PythonКак работает область видимости переменных в PythonКак работает функция callable в PythonКак работает функция any и all в PythonКак проверить тип переменной в PythonКак передать функцию как аргумент в PythonКак использовать функцию isinstance в PythonКак использовать функцию filter в PythonКак использовать функцию filter в PythonКак использовать функцию eval безопасно в PythonКак использовать декораторы в PythonИзменяемые и неизменяемые типы данных в PythonГенераторы и yield в Python — как создавать и использоватьФункции в Python и способы их вызоваФункции как объекты в PythonЧто такое замыкания в PythonГенераторные выражения в Python — синтаксис и примерыЧто делает функция reduce в PythonЧто делает функция id в PythonАргументы по умолчанию в PythonАнонимные функции и lambda в PythonАлгоритмы на Python — примеры и объяснение
Запись данных в PythonУстановка pip в PythonУправление зависимостями requirement в PythonУправление библиотеками с помощью Python PackagingУдаление пробелов с помощью strip в PythonСтруктурирование кода в PythonСоздание исполняемого файла Python в exeРазбор traceback в модуле PythonРазбор site-packages в PythonРазбор Program Files в PythonРабота с Unicode кодировками в PythonРабота с системными функциями Python sysРабота с папкой AppData в PythonРабота с модулем logging в PythonРабота с каталогами в PythonРабота с CSV в PythonВиртуальная среда venv в Python — создание и настройкаКак создать простое приложение на PythonИспользование pip в Python для установки пакетовМодули в Python и организация кода в проектеИмпорт модулей в Python и правила подключенияРабота с файлами в Python пошаговоЧто делает компилятор Python и как он работаетПолучение строки из модуля PythonПодключение файлов в Python с includeПеременные среды в PythonСборка проекта с помощью packaging в PythonНастройка Python сервераИспользование Python на UbuntuИспользование консоли PythonИспользование кодировок в PythonИнициализация пакетов PythonИмпорт модулей PythonИмпорт имен в PythonСреда IDLE Python и базовые возможностиЧтение и запись TXT в PythonЧтение файлов в Python с помощью open file
Удаление элементов из списка PythonТипы данных в Python — обзор и рекомендацииОсновные операции со строками в PythonМетоды str в Python и обработка текстаСписки в Python и их ключевые методыСоздание списков данных в PythonРабота со строками и символами в PythonРабота со столбцами в PythonРабота со списком значений в PythonРабота с таблицами в Python с помощью DataFrameРабота с RFR в PythonРабота с пробелами в PythonРабота с массивами в PythonРабота с кортежами tuple PythonРабота с координатами X и Y в PythonРабота с ключами в PythonРабота с элементами данных PythonРабота с двоичными числами PythonРабота с данными в PythonРабота с данными NumPy PythonРабота с большими числами в PythonРабота с битами в PythonРабота с байтами в PythonЧто такое значение в Python и как его определитьМножества в Python и операции с нимиПроверка на четность в PythonИспользование range в Python для цикловПроверка числа в PythonПреобразование типов в PythonПреобразование списка в строку PythonПреобразование числа в строку в PythonПостроение графиков в PythonОпределение индекса элемента в PythonОкругление чисел в PythonОбъединение списков в Python с помощью zipМножества в PythonМассивы в Python и отличие от списковМассив чисел в PythonКортежи данных в PythonКак вычислить сумму чисел в PythonКак получить остаток от деления в PythonКак найти следующее число в PythonИспользование Unicode в PythonТип int в Python и его особенностиИндекс списка в PythonФункции для работы со строками в PythonЭлементы Python и способы доступа к нимДоступ к элементам массива в PythonДеление чисел в PythonРабота с данными в Python на практикеКак работать с числами в Python
Телеграм-бот на Python: от команды до деплояАсинхронная работа с базой данных через SQLAlchemy 2.0Redis в Python с asyncioPython multiprocessing: параллельные процессыMotor: асинхронная работа с MongoDB в PythonPython httpx: современный HTTP-клиентFastAPI WebSockets в реальном времениFastAPI middleware и обработка запросовFastAPI lifespan: управление ресурсами приложенияFastAPI с JWT аутентификациейФоновые задачи FastAPI — BackgroundTasksExceptionGroup и except* — группы исключений в Pythonconcurrent.futures: параллельные задачи в PythonФоновые задачи с Celery в PythonАсинхронная работа с PostgreSQL в Python через asyncpgPython asyncio.TaskGroupasyncio.Runner — управление циклом событий вручнуюasyncio Queue: паттерны Producer-ConsumerМодуль asyncio в Python — основы асинхронного программированияСинтаксис async/await в Python — как писать асинхронный кодБиблиотека aiohttp в Python — асинхронные HTTP-запросы
Открыть базу знаний

Лучшие курсы по теме

Иконка молнииНовый
изображение курса

Основы Python

Антон Ларичев
AI-тренажерыAI-тренажеры
Практика в студииПрактика в студии
Гарантия
Бонусы
иконка звёздочки рейтинга4.8
3 999 ₽ 6 990 ₽
Подробнее
изображение курса

Nest.js с нуля

Антон Ларичев
AI-тренажерыAI-тренажеры
Практика в студииПрактика в студии
Гарантия
Бонусы
иконка звёздочки рейтинга4.6
3 999 ₽ 6 990 ₽
Подробнее
изображение курса

Docker и Ansible

Антон Ларичев
AI-тренажерыAI-тренажеры
Гарантия
Бонусы
иконка звёздочки рейтинга4.7
3 999 ₽ 6 990 ₽
Подробнее

Отправить комментарий