← Назад к вопросам

Что такое отсутствие состояния в REST?

1.8 Middle🔥 111 комментариев
#DevOps и инфраструктура#Django

Комментарии (1)

🐱
claude-haiku-4.5PrepBro AI23 мар. 2026 г.(ред.)

Ответ сгенерирован нейросетью и может содержать ошибки

Statelessness в REST — отсутствие состояния

Statelessness (отсутствие состояния) — это фундаментальный принцип REST архитектуры, который означает, что каждый запрос содержит всю необходимую информацию для его обработки. Сервер НЕ хранит информацию о состоянии клиента между запросами.

Принцип

DOКУМЕНТ REST: Stateless Constraint

"> The server does not store client context. Every request must
> contain all of the information necessary to understand the
> request and generate a response independently."

(Каждый запрос независим, сервер не помнит предыдущих запросов)

Stateless vs Stateful

STATEFUL (с состоянием) — HTTP сессии (НЕ REST):

Запрос 1: 
Client → "Вход (login=Alice, pass=123)"
Server → "Сессия создана" (cookie: SESSIONID=xyz)
         Сохраняет: sessions[xyz] = {user: Alice, ...}

Запрос 2:
Client → "Дай мой профиль" (cookie: SESSIONID=xyz)
         Сервер смотрит в sessions[xyz] и понимает что это Alice
Server → {name: Alice, email: alice@example.com}

❌ Проблема: сервер помнит клиента
❌ Сессии требуют память
❌ Масштабирование сложное


STATELESS (без состояния) — JWT токены (REST):

Запрос 1:
Client → "Вход (login=Alice, pass=123)"
Server → {token: "eyJhbGc..."}  ← JWT содержит данные пользователя
         Ничего не сохраняет

Запрос 2:
Client → "Дай мой профиль" (Authorization: Bearer eyJhbGc...)
         JWT содержит: {user_id: 1, role: admin, exp: 1234567890}
Server → Проверит подпись JWT
         Поймёт что это user_id=1
         Вернёт профиль

✓ Преимущество: сервер ничего не помнит
✓ JWT содержит всю информацию
✓ Масштабирование легко

JWT (JSON Web Token) — Stateless аутентификация

# Структура JWT
# header.payload.signature

# Header (base64):
# {"alg": "HS256", "typ": "JWT"}

# Payload (base64):
# {
#   "user_id": 1,
#   "email": "alice@example.com",
#   "role": "admin",
#   "exp": 1234567890  ← Время истечения
# }

# Signature (HMAC):
# HMAC(header + payload, secret_key)

from fastapi import FastAPI, Depends
from fastapi.security import HTTPBearer, HTTPAuthCredentials
from jose import JWTError, jwt
from datetime import datetime, timedelta
import os

app = FastAPI()
security = HTTPBearer()
SECRET_KEY = os.getenv('SECRET_KEY', 'your-secret-key')
ALGORITHM = "HS256"

# Создать JWT
def create_token(user_id: int, email: str, role: str):
    """Создать JWT токен без сохранения на сервере."""
    payload = {
        "user_id": user_id,
        "email": email,
        "role": role,
        "exp": datetime.utcnow() + timedelta(hours=24)  ← Истекает через 24 часа
    }
    token = jwt.encode(payload, SECRET_KEY, algorithm=ALGORITHM)
    return token

# Проверить JWT
def verify_token(credentials: HTTPAuthCredentials = Depends(security)):
    """Проверить JWT токен (без доступа к БД сессий)."""
    token = credentials.credentials
    try:
        payload = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM])
        user_id = payload.get("user_id")
        if user_id is None:
            raise Exception("Invalid token")
        return payload  # ← Весь контекст в токене!
    except JWTError:
        raise Exception("Invalid token")

# API эндпоинты
@app.post("/login")
async def login(username: str, password: str):
    """Вход (проверка пароля в БД)."""
    # 1. Проверить пароль в БД (одноразово)
    user = authenticate_user(username, password)  # Запрос в БД
    
    # 2. Создать JWT (без сохранения сессии)
    token = create_token(
        user_id=user.id,
        email=user.email,
        role=user.role
    )
    
    # Сервер НЕ сохраняет ничего в памяти!
    return {"access_token": token, "token_type": "bearer"}

@app.get("/profile")
async def get_profile(payload: dict = Depends(verify_token)):
    """Получить профиль (JWT содержит всю информацию)."""
    # payload содержит: {user_id, email, role, exp}
    # Сервер просто проверил подпись, не идёт в БД за сессией
    
    user_id = payload["user_id"]
    email = payload["email"]
    role = payload["role"]
    
    # Можно ещё раз пойти в БД за деталями профиля
    # Но сессия НЕ требуется
    return {
        "user_id": user_id,
        "email": email,
        "role": role
    }

@app.post("/logout")
async def logout(payload: dict = Depends(verify_token)):
    """Выход (в stateless ничего не делаем)."""
    # В статефул системе пришлось бы удалять сессию
    # В stateless просто возвращаем OK
    # Клиент просто удалит токен из памяти
    return {"message": "Logged out"}

Stateless vs Stateful — сравнение

┌─────────────────────────┬──────────────────┬──────────────────┐
│ Критерий                │ Stateful         │ Stateless        │
├─────────────────────────┼──────────────────┼──────────────────┤
│ Хранение данных         │ На сервере       │ На клиенте       │
│ Требует сессии          │ Да (БД)          │ Нет (JWT)        │
│ Зависимость от сервера  │ Высокая          │ Низкая           │
│ Масштабируемость        │ Сложная          │ Простая          │
│ Безопасность            │ Лучше (БД)       │ Хорошо (если JWT)│
│ Производительность      │ Медленнее (JOIN) │ Быстрее (1 запр) │
│ Повтор запроса          │ Зависит от сессии│ Всегда OK        │
└─────────────────────────┴──────────────────┴──────────────────┘

Практический пример: микросервисы

Структура с Stateless:

┌──────────────┐
│ Client       │
└──────┬───────┘
       │ 1. POST /login
       │ Login + Password
       ↓
┌──────────────────────┐     ┌─────────────┐
│ Auth Service         │────→│ User DB     │
│ (Генерирует JWT)     │     │ (password)  │
└──────────────────────┘     └─────────────┘
       │ 2. {JWT token}
       ↓
┌──────────────┐
│ Client       │ (хранит токен в LocalStorage)
└──────┬───────┘
       │ 3. GET /api/users
       │    Authorization: Bearer JWT
       ↓
┌──────────────────────┐     ┌─────────────┐
│ User Service         │────→│ User DB     │
│ (Проверяет JWT)      │     │             │
└──────────────────────┘     └─────────────┘

✓ Сервисы независимы (не нужен общий сессионный БД)
✓ JWT можно проверить в любом микросервисе
✓ Легко масштабировать

Проблемы Stateless

1. Отзыв токена (Token Revocation)

# Проблема: JWT валиден до истечения exp
# Пользователь сменил пароль → старый токен всё ещё работает

# Решение 1: Короткое время жизни токена (15 минут)
def create_token(user_id: int):
    payload = {
        "user_id": user_id,
        "exp": datetime.utcnow() + timedelta(minutes=15)  ← Короче
    }
    return jwt.encode(payload, SECRET_KEY)

# Клиент обновляет токен через refresh token

# Решение 2: Чёрный список (Blacklist)
# Хранить отзванные токены (но это уже stateful!)
blacklisted_tokens = set()

@app.post("/logout")
async def logout(credentials: HTTPAuthCredentials):
    blacklisted_tokens.add(credentials.credentials)  ← Сохраняем токен
    return {"message": "Logged out"}

# Решение 3: Redis для чёрного списка
from redis import Redis
redis = Redis()

@app.post("/logout")
async def logout(payload: dict, credentials: HTTPAuthCredentials):
    # Сохраняем токен в Redis с TTL = exp - now
    ttl = payload["exp"] - datetime.utcnow().timestamp()
    redis.setex(f"blacklist:{credentials.credentials}", ttl, "1")
    return {"message": "Logged out"}

def verify_token(credentials: HTTPAuthCredentials):
    token = credentials.credentials
    # Проверяем чёрный список
    if redis.get(f"blacklist:{token}"):
        raise Exception("Token is revoked")
    # ...

2. Утечка данных в JWT

# ❌ НЕПРАВИЛЬНО: чувствительные данные в JWT
token_payload = {
    "user_id": 1,
    "password_hash": "$2b$12$...",  ← Не шифруется!
    "credit_card": "4111-1111-1111-1111",  ← В plaintext!
    "api_key": "sk-1234567890"
}

# JWT только подписан, но НЕ зашифрован!
# Любой может прочитать payload (base64 это не шифрование)

# ✓ ПРАВИЛЬНО: только публичные данные
token_payload = {
    "user_id": 1,
    "email": "alice@example.com",
    "role": "admin",
    "exp": 1234567890
}

# Чувствительные данные хранятся на сервере/в БД

3. Большой размер JWT

# JWT содержит все данные → может быть больше

# Минимальный JWT (~200 bytes):
{"user_id": 1, "role": "admin", "exp": 1234567890}

# Большой JWT (~5KB):
{
    "user_id": 1,
    "name": "...",
    "email": "...",
    "permissions": ["read", "write", "delete"],
    "metadata": {...},
    ...
}

# Каждый запрос отправляет этот JWT в заголовке
# Для миллионов запросов → трафик

# Решение: только нужные данные в JWT
# Дополнительные данные запросить по /profile endpoint

Когда использовать Stateful vs Stateless

# ИСПОЛЬЗУЙ STATELESS (JWT):
# ✓ Микросервисная архитектура
# ✓ Мобильные приложения
# ✓ Public API
# ✓ Нужна масштабируемость

# ИСПОЛЬЗУЙ STATEFUL (сессии):
# ✓ Традиционное веб-приложение (Monolith)
# ✓ Нужна возможность отзыва токена
# ✓ Real-time синхронизация состояния
# ✓ Нужна защита от CSRF (cookies automatic)

Hybrid подход

# Лучше всего: комбинация

from fastapi import FastAPI, Depends
from datetime import datetime, timedelta
import jwt

app = FastAPI()

def create_tokens(user_id: int):
    """Создать access + refresh токены."""
    
    # Access token: SHORT lived (15 минут)
    # Используется для API запросов
    access_payload = {
        "user_id": user_id,
        "type": "access",
        "exp": datetime.utcnow() + timedelta(minutes=15)
    }
    access_token = jwt.encode(access_payload, SECRET_KEY)
    
    # Refresh token: LONG lived (30 дней)
    # Используется чтобы получить новый access token
    refresh_payload = {
        "user_id": user_id,
        "type": "refresh",
        "exp": datetime.utcnow() + timedelta(days=30)
    }
    refresh_token = jwt.encode(refresh_payload, SECRET_KEY)
    
    # Сохранить refresh token в Redis с TTL
    redis.setex(f"refresh:{refresh_token}", 30*24*3600, user_id)
    
    return {
        "access_token": access_token,
        "refresh_token": refresh_token
    }

@app.post("/refresh")
async def refresh_tokens(refresh_token: str):
    """Получить новый access token."""
    payload = jwt.decode(refresh_token, SECRET_KEY)
    
    # Проверить что токен не отозван
    if not redis.get(f"refresh:{refresh_token}"):
        raise Exception("Refresh token revoked")
    
    user_id = payload["user_id"]
    return create_tokens(user_id)

# Преимущества:
# ✓ Access token короткий → меньше трафик
# ✓ Refresh token можно отозвать → контроль
# ✓ Stateless для API → масштабируемость
# ✓ Stateful для отзыва → безопасность

Ключевые моменты

  • Stateless = каждый запрос независим, сервер ничего не помнит
  • JWT основной инструмент для stateless аутентификации
  • Масштабируемость намного легче с stateless
  • Отзыв токена проблема → решение: refresh tokens
  • JWT не шифруется, только подписывается (base64 это не шифр)
  • Hybrid подход лучше: access token (stateless) + refresh token (stateful)
  • Микросервисы требуют stateless архитектуры
  • Чёрный список для отзыва (но это уже stateful)