JWT и FastAPI: как студент-системщик перестал бояться веб-авторизации

Вступление

Обычно я копаюсь в системных вызовах Linux, балуюсь с оконным менеджером bspwm или пишу многопоточные штуки на C++. Веб-разработка долгое время казалась мне чем-то вроде чёрной магии: куча декораторов, dependency injection и бесконечные JSON'ы. Но когда потребовалось запилить бэкенд для одного пет-проекта (бота для Twitch, если быть точным), я решил, что пора закрыть этот гештальт. Выбор пал на FastAPI — говорят, быстрый и асинхронный. А где FastAPI, там и вопрос авторизации. В этой статье я хочу рассказать, как разбирался с JWT, и показать, что даже если вы всю жизнь писали на плюсах, разобраться с этим вполне реально.

Теория: что за зверь этот JWT?

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

Токен состоит из трёх частей, разделённых точками: header.payload.signature.

  • Header: говорит, что это JWT и каким алгоритмом подписан (например, HS256).
  • Payload: полезная нагрузка — тут лежит ID пользователя, его роль и время жизни токена (exp). В отличие от SSH, эта часть не шифруется, а только кодируется в base64. Поэтому никогда не кладите туда пароли или номера кредиток.
  • Signature: самое главное. Сервер берёт header и payload, добавляет свой секретный ключ и прогоняет через HMAC-SHA256. Если злоумышленник попытается поменять user_id в payload, подпись не сойдётся, и сервер сразу пошлёт его лесом.

В реальных проектах обычно используют два токена: Access Token (живёт минут 15-30) и Refresh Token (может жить неделями). Это сделано для того, чтобы даже если Access Token угонят, ущерб был ограничен по времени, а пользователю не приходилось вводить пароль каждый раз.

Практика: собираем всё в коде

Я не люб, когда в туториалах пишут "просто скопируйте эти 100 строк и оно заработает". Мне всегда хочется понять структуру. Поэтому я разбил проект на слои, как в нормальном софте.

Кстати, для всех, кто привык к CMake и makefile'ам, структура папок в Python поначалу кажется хаосом, но к этому быстро привыкаешь.
Кстати, для всех, кто привык к CMake и makefile'ам, структура папок в Python поначалу кажется хаосом, но к этому быстро привыкаешь.

1. Конфигурация (app/core/config.py)

Тут всё просто. Я вынес все настройки в переменные окружения, чтобы не светить секретный ключ в репозитории.

from pydantic_settings import BaseSettings class Settings(BaseSettings): SECRET_KEY: str = "your-super-secret-key" # В реальности берётся из .env ALGORITHM: str = "HS256" ACCESS_TOKEN_EXPIRE_MINUTES: int = 30 REFRESH_TOKEN_EXPIRE_DAYS: int = 7 class Config: env_file = ".env" settings = Settings()

2. Работа с токенами (app/core/security.py)

Вот здесь и происходит вся "магия". Я написал две простые функции: одна создаёт токен, другая — проверяет его и достаёт пользователя.

from datetime import datetime, timedelta, timezone from jose import jwt, JWTError from app.core.config import settings def create_access_token(data: dict): to_encode = data.copy() expire = datetime.now(timezone.utc) + timedelta(minutes=settings.ACCESS_TOKEN_EXPIRE_MINUTES) to_encode.update({"exp": expire, "type": "access"}) encoded_jwt = jwt.encode(to_encode, settings.SECRET_KEY, algorithm=settings.ALGORITHM) return encoded_jwt # Функция для refresh_token аналогична

3. Эндпоинт для логина (app/routers/auth.py)

Здесь FastAPI красиво обрабатывает форму логина и возвращает пару токенов. Обратите внимание на OAuth2PasswordRequestForm — это встроенная зависимость, которая сама разбирает application/x-www-form-urlencoded.

from fastapi import APIRouter, Depends, HTTPException, status from fastapi.security import OAuth2PasswordRequestForm router = APIRouter(prefix="/auth", tags=["auth"]) @router.post("/login") async def login(form_data: OAuth2PasswordRequestForm = Depends()): # Тут должна быть проверка пользователя в БД # if not user or not verify_password(...): raise ... access_token = create_access_token(data={"sub": form_data.username}) refresh_token = create_refresh_token(data={"sub": form_data.username}) return {"access_token": access_token, "refresh_token": refresh_token, "token_type": "bearer"}

4. Защита эндпоинтов (app/core/security.py и app/routers/users.py)

Самая крутая фишка FastAPI — это dependency injection с Depends. Мы создаём функцию get_current_user, которая будет автоматически вызываться перед каждым защищённым эндпоинтом. Она достаёт токен из заголовка Authorization: Bearer <токен>, проверяет его и возвращает данные о пользователе.

from fastapi import Depends, HTTPException, status from fastapi.security import OAuth2PasswordBearer from jose import JWTError, jwt from app.core.config import settings oauth2_scheme = OAuth2PasswordBearer(tokenUrl="/auth/login") async def get_current_user(token: str = Depends(oauth2_scheme)): credentials_exception = HTTPException(status_code=status.HTTP_401_UNAUTHORIZED) try: payload = jwt.decode(token, settings.SECRET_KEY, algorithms=[settings.ALGORITHM]) username: str = payload.get("sub") if username is None: raise credentials_exception except JWTError: raise credentials_exception # Тут можно достать пользователя из БД по username # return user

Использовать это проще простого:

from fastapi import APIRouter, Depends from app.core.security import get_current_user router = APIRouter(prefix="/users", tags=["users"]) @router.get("/me") async def read_users_me(current_user = Depends(get_current_user)): return {"username": current_user}

Заключение

Для человека, привыкшего к fork() и epoll, мир веб-фреймворков поначалу кажется перегруженным абстракциями. Но если копнуть чуть глубже, видишь те же базовые принципы: клиент-сервер, запрос-ответ и необходимость в надёжной аутентификации. FastAPI с его dependency injection и встроенной поддержкой JWT делает этот процесс удивительно простым и понятным. Теперь я могу сосредоточиться на логике своего бота, а не на изобретении велосипеда для авторизации. Надеюсь, мой опыт поможет и вам быстрее вкатиться в эту тему.

P.S. Если вы, как и я, любите копаться в исходниках и системных вызовах, рекомендую всё же попробовать FastAPI. Это хороший пример того, как высокоуровневые абстракции могут экономить время, не скрывая при этом сути происходящего.

1