JWT авторизация в FastAPI: от теории к практике
Введение: почему сессии больше не нужны?
Представьте себе, что вы разработчик, и перед вами стоит задача: сделать так, чтобы после входа пользователь мог получать свои личные данные, не вводя пароль при каждом клике. Звучит как классическая задача аутентификации, правда?
Традиционный подход — сессии. Вы логинитесь, сервер создаёт сессию, запоминает ваш ID у себя в базе данных, а вам выдаёт куку с ID этой сессии. Всё работает, пока вы на одном сервере. А если у вас их два? Или десять? Куда девать сессии? Начинаются проблемы с синхронизацией, Redis, общими хранилищами…
Альтернатива — JWT (JSON Web Token). В этой статье мы с вами:
Разберём, что такое JWT и как он устроен
Напишем полноценный проект на FastAPI с нуля
Реализуем регистрацию, логин и защищённые эндпоинты
Разберём каждый кусочек кода, чтобы вы понимали, что происходит
И самое главное — я покажу полный путь запроса: от момента, когда вы нажимаете кнопку “Войти”, до того, как получаете ответ от сервера.
Часть 1. Что такое JWT и с чем его едят
JWT — это просто строка
JWT (читается как “джот”) — это JSON Web Token. На практике это просто длинная строка, которая выглядит примерно так:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJhbGV4IiwibmFtZSI6IkFsZXggVG9tY2hvayIsImlhdCI6MTUxNjIzOTAyMiwiZXhwIjoxNTE2MjQyNjIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c
Но внутри этой строки скрывается структурированная информация. Разберём её по слоям.
Структура JWT
JWT состоит из трёх частей, разделённых точками:
Header (заголовок) — информация о том, как подписан токен
Payload (полезная нагрузка) — сами данные, которые мы хотим передать
Signature (подпись) — результат шифрования заголовка и payload с нашим секретным ключом
Если декодировать (не расшифровать, а просто перевести из base64 в читаемый вид) наш токен, получим:
Header:
{ "alg": "HS256", "typ": "JWT" }
Payload:
{ "sub": "alex", "name": "Alex Tomchok", "iat": 1516239022, "exp": 1516242622 }
sub(subject) — обычно здесь хранится идентификатор пользователяiat(issued at) — время создания токенаexp(expiration) — время истечения
Почему JWT безопасен?
Самое важное: подпись. Когда сервер создаёт токен, он берёт заголовок + payload, добавляет секретный ключ (который знает только сервер) и создаёт подпись. При следующем запросе сервер снова берёт заголовок + payload, добавляет свой секретный ключ и сравнивает получившуюся подпись с той, что пришла в токене.
Если кто-то изменит данные в payload, подпись перестанет совпадать, и сервер отклонит токен. Данные в JWT не защищены от чтения — их может прочитать кто угодно, но изменить — никто.
Access token vs Refresh token
В реальных проектах используют два типа токенов:
Access token — живёт недолго (15-30 минут). Используется для доступа к API. Если его украдут, злоумышленник будет иметь доступ ограниченное время.
Refresh token — живёт долго (дни или месяцы). Используется только для получения нового access token, когда старый истёк.
В нашем проекте для простоты будем использовать только access token.
Часть 2. Готовим окружение
Создадим структуру проекта:
fastapi-jwt-auth/ ├── app/ │ ├── __init__.py │ ├── main.py │ ├── core/ │ │ ├── __init__.py │ │ ├── config.py │ │ └── security.py │ ├── schemas/ │ │ ├── __init__.py │ │ ├── token.py │ │ └── user.py │ ├── api/ │ │ ├── __init__.py │ │ └── v1/ │ │ ├── __init__.py │ │ ├── endpoints/ │ │ │ ├── __init__.py │ │ │ ├── auth.py │ │ │ └── users.py │ │ └── dependencies.py │ └── services/ │ ├── __init__.py │ └── user_service.py └── requirements.txt
requirements.txt:
fastapi==0.104.1 uvicorn[standard]==0.24.0 python-jose[cryptography]==3.3.0 passlib[bcrypt]==1.7.4 python-multipart==0.0.6 pydantic[email]==2.5.0
Устанавливаем:
pip install -r requirements.txt
Часть 3. Архитектурная схема работы программы

Часть 4. Пишем код шаг за шагом
4.1. Конфигурация (core/config.py)
Начнём с настроек. Всё, что может меняться в зависимости от окружения, выносим в переменные:
from pydantic_settings import BaseSettings class Settings(BaseSettings): # Секретный ключ — в реальном проекте брать из .env! SECRET_KEY: str = "your-super-secret-key-change-this-in-production" ALGORITHM: str = "HS256" ACCESS_TOKEN_EXPIRE_MINUTES: int = 30 class Config: env_file = ".env" settings = Settings()
Важно: В production секретный ключ должен храниться в переменных окружения, а не в коде!
4.2. Pydantic схемы (schemas/)
Схемы — это контракты. Они говорят: “Вот такие данные мы принимаем, и вот такие отдаём”.
schemas/user.py:
from pydantic import BaseModel, EmailStr class UserBase(BaseModel): username: str email: EmailStr class UserCreate(UserBase): password: str class User(UserBase): id: int is_active: bool = True class Config: from_attributes = True class UserInDB(User): hashed_password: str
Обратите внимание на from_attributes = True — это позволяет создавать Pydantic модели из SQLAlchemy моделей (или любых других объектов с атрибутами).
schemas/token.py:
from pydantic import BaseModel class Token(BaseModel): access_token: str token_type: str = "bearer" class TokenData(BaseModel): username: str | None = None
4.3. Безопасность (core/security.py)
Здесь происходит вся магия с хешированием паролей и JWT.
from datetime import datetime, timedelta from jose import JWTError, jwt from passlib.context import CryptContext from .config import settings # Настройка хеширования паролей (bcrypt — один из самых надёжных алгоритмов) 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) -> str: """Создаёт JWT токен с данными из data""" to_encode = data.copy() expire = datetime.utcnow() + timedelta(minutes=settings.ACCESS_TOKEN_EXPIRE_MINUTES) to_encode.update({"exp": expire}) encoded_jwt = jwt.encode( to_encode, settings.SECRET_KEY, algorithm=settings.ALGORITHM ) return encoded_jwt def decode_access_token(token: str) -> dict: """Декодирует токен и возвращает payload, если подпись верна""" try: payload = jwt.decode( token, settings.SECRET_KEY, algorithms=[settings.ALGORITHM] ) return payload except JWTError: return None
4.4. Сервис пользователей (services/user_service.py)
В реальном проекте здесь будет работа с базой данных. Для демонстрации используем простой список в памяти:
from app.schemas.user import UserInDB from app.core.security import verify_password, get_password_hash # Наша "база данных" fake_users_db = { "alex": { "id": 1, "username": "alex", "email": "alex@example.com", "hashed_password": get_password_hash("secret123"), "is_active": True, } } def get_user_by_username(username: str) -> UserInDB | None: """Ищет пользователя по имени""" if username in fake_users_db: return UserInDB(**fake_users_db[username]) return None def authenticate_user(username: str, password: str) -> UserInDB | None: """Аутентифицирует пользователя: проверяет, что такой есть и пароль верный""" user = get_user_by_username(username) if not user: return None if not verify_password(password, user.hashed_password): return None return user
4.5. Зависимости для авторизации (api/v1/dependencies.py)
Зависимости в FastAPI — это мощный инструмент. Они позволяют вынести повторяющуюся логику (например, получение текущего пользователя) в отдельные функции.
from fastapi import Depends, HTTPException, status from fastapi.security import OAuth2PasswordBearer from app.core.security import decode_access_token from app.services.user_service import get_user_by_username from app.schemas.token import TokenData from app.schemas.user import User # OAuth2PasswordBearer — встроенная зависимость FastAPI # Она извлекает токен из заголовка Authorization: Bearer <token> oauth2_scheme = OAuth2PasswordBearer(tokenUrl="/api/v1/auth/login") async def get_current_user(token: str = Depends(oauth2_scheme)) -> User: """ Извлекает текущего пользователя из JWT токена. Это сердце нашей авторизации! """ credentials_exception = HTTPException( status_code=status.HTTP_401_UNAUTHORIZED, detail="Could not validate credentials", headers={"WWW-Authenticate": "Bearer"}, ) # Декодируем токен payload = decode_access_token(token) if payload is None: raise credentials_exception # Извлекаем имя пользователя username: str = payload.get("sub") if username is None: raise credentials_exception # Проверяем, что пользователь существует user = get_user_by_username(username) if user is None: raise credentials_exception return user async def get_current_active_user( current_user: User = Depends(get_current_user) ) -> User: """Проверяет, что пользователь не заблокирован""" if not current_user.is_active: raise HTTPException( status_code=status.HTTP_400_BAD_REQUEST, detail="Inactive user" ) return current_user
4.6. Эндпоинты аутентификации (api/v1/endpoints/auth.py)
Теперь создадим точку входа — эндпоинт /login. Он принимает username и password, проверяет их и выдёт JWT токен.
from fastapi import APIRouter, Depends, HTTPException, status from fastapi.security import OAuth2PasswordRequestForm from app.schemas.token import Token from app.services.user_service import authenticate_user from app.core.security import create_access_token router = APIRouter(prefix="/auth", tags=["authentication"]) @router.post("/login", response_model=Token) async def login(form_data: OAuth2PasswordRequestForm = Depends()): """ Логин пользователя. Принимает username и password (form-data), возвращает JWT токен. """ # Проверяем пользователя user = authenticate_user(form_data.username, form_data.password) if not user: raise HTTPException( status_code=status.HTTP_401_UNAUTHORIZED, detail="Incorrect username or password", headers={"WWW-Authenticate": "Bearer"}, ) # Создаём токен access_token = create_access_token(data={"sub": user.username}) return Token(access_token=access_token, token_type="bearer")
4.7. Защищённые эндпоинты (api/v1/endpoints/users.py)
А вот и защищённый эндпоинт. Обратите внимание на зависимость — она сама позаботится о проверке токена.
from fastapi import APIRouter, Depends from app.schemas.user import User from app.api.v1.dependencies import get_current_active_user router = APIRouter(prefix="/users", tags=["users"]) @router.get("/me", response_model=User) async def read_users_me(current_user: User = Depends(get_current_active_user)): """ Возвращает информацию о текущем пользователе. Требует валидный JWT токен в заголовке Authorization. """ return current_user
4.8. Сборка приложения (main.py)
Всё собираем вместе:
from fastapi import FastAPI from fastapi.middleware.cors import CORSMiddleware from app.api.v1.endpoints import auth, users app = FastAPI( title="FastAPI JWT Auth Demo", description="Пример реализации JWT авторизации на FastAPI", version="1.0.0" ) # CORS для разработки app.add_middleware( CORSMiddleware, allow_origins=["*"], allow_credentials=True, allow_methods=["*"], allow_headers=["*"], ) # Регистрируем роутеры app.include_router(auth.router, prefix="/api/v1") app.include_router(users.router, prefix="/api/v1") @app.get("/") async def root(): return {"message": "FastAPI JWT Auth Demo. Go to /docs for API documentation"} if __name__ == "__main__": import uvicorn uvicorn.run(app, host="0.0.0.0", port=8000)
Часть 5. Полный путь запроса (по шагам)
Давайте проследим, что происходит, когда пользователь логинится, а потом обращается к защищённому эндпоинту.
5.1. Шаг 1: Логин (получение токена)
1. Клиент отправляет запрос:
POST http://localhost:8000/api/v1/auth/login Content-Type: application/x-www-form-urlencoded username=alex&password=secret123
2. FastAPI принимает запрос и направляет его в auth.login().
3. Валидация: OAuth2PasswordRequestForm автоматически проверяет, что поля username и password пришли и не пустые.
4. Аутентификация: Вызывается authenticate_user(), которая:
Ищет пользователя в базе
Проверяет пароль через
verify_password()
5. Генерация токена: Вызывается create_access_token(), которая:
Создаёт payload:
{"sub": "alex", "exp": <текущее_время+30минут>}Подписывает его секретным ключом
Возвращает JWT строку
6. Ответ клиенту:
{ "access_token": "eyJhbGciOiJIUzI1NiIs...", "token_type": "bearer" }
5.2. Шаг 2: Доступ к защищённому ресурсу
1. Клиент отправляет запрос с токеном:
GET http://localhost:8000/api/v1/users/me Authorization: Bearer eyJhbGciOiJIUzI1NiIs...
2. FastAPI направляет запрос в users.read_users_me().
3. Вызывается зависимость get_current_active_user().
4. Внутри зависимости:
get_current_user()извлекает токен из заголовкаДекодирует и проверяет подпись через
decode_access_token()Проверяет срок действия (
exp)Из payload’а достаёт
sub(username)Находит пользователя в базе
Возвращает объект пользователя
5. get_current_active_user() проверяет, что пользователь не заблокирован.
6. Выполняется бизнес-логика эндпоинта — возвращает данные пользователя.
7. Ответ клиенту:
{ "id": 1, "username": "alex", "email": "alex@example.com", "is_active": true }
Часть 6. Как это выглядит на практике (проверка через Swagger)
FastAPI автоматически генерирует документацию по адресу /docs. Вот как работает авторизация в Swagger:
Перейдите на
http://localhost:8000/docsНайдите эндпоинт
/auth/login, нажмите “Try it out”Введите
usernameиpassword, выполните запросСкопируйте полученный
access_tokenНажмите кнопку “Authorize” вверху, введите
Bearer <ваш_токен>Теперь можете выполнять запрос к
/users/me— токен будет автоматически добавлен в заголовки
Часть 7. Подводные камни и лучшие практики
1. Где хранить секретный ключ?
Плохо:
SECRET_KEY = "my-super-secret-key" # прямо в коде!
Хорошо:
import os SECRET_KEY = os.getenv("JWT_SECRET_KEY") # из переменных окружения
Создайте файл .env и добавьте его в .gitignore:
JWT_SECRET_KEY=your-super-secret-key-here
2. Какой срок жизни токена выбирать?
15–30 минут — стандарт для access token
Если приложение требует долгой сессии — добавьте refresh token
3. Как обновлять токен?
Самый простой способ — refresh token:
Клиент хранит refresh token (например, в httpOnly cookie)
Когда access token истекает, клиент отправляет refresh token на
/refreshСервер проверяет refresh token и выдаёт новый access token
4. Всегда проверяйте подпись!
Никогда не доверяйте данным из payload, пока не проверили подпись токена. Злоумышленник может изменить sub на “admin”, и если вы просто прочитаете это поле без проверки — вы получите несанкционированный доступ.
Заключение
Мы с вами:
Разобрались, что такое JWT и как он устроен
Написали полноценное приложение на FastAPI с JWT авторизацией
Прошли полный путь запроса от начала до конца
Разобрали лучшие практики безопасности
Теперь вы знаете, как построить авторизацию, которая не требует хранения сессий на сервере и легко масштабируется на сотни инстансов приложения.
Код из статьи доступен на GitHub: https://github.com/MaximBytecamp/js_projects/tree/9ade6e5616b8bbf523ac938bdc65d7db9c9e6585/fastapi-demo
Если у вас остались вопросы или вы нашли неточность — пишите в комментариях. Вместе сделаем код лучше!
