← Назад к вопросам
Что такое отсутствие состояния в 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)