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

Как происходит клиент серверное взаимодействие с JWT?

2.3 Middle🔥 231 комментариев
#REST API и HTTP#Безопасность

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

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

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

Клиент-серверное взаимодействие с JWT

JWT позволяет реализовать безопасную аутентификацию в REST API с минимальным взаимодействием между клиентом и сервером. Разберём полный цикл взаимодействия.

Архитектура взаимодействия

Клиент (браузер/мобильное приложение)
    ↓
Сервер аутентификации (выдача токена)
    ↓
Остальные серверы API (валидация токена)

Шаг 1: Получение токена (Login)

Запрос клиента:

import requests
import json

# Клиент отправляет учетные данные
response = requests.post(
    "https://api.example.com/api/v1/auth/login",
    json={"username": "john", "password": "secret123"}
)

data = response.json()
token = data["access_token"]  # JWT токен
local_storage["token"] = token  # Сохраняем локально

Ответ сервера:

from fastapi import FastAPI, HTTPException
from datetime import datetime, timedelta, timezone
import jwt

app = FastAPI()
SECRET_KEY = "your-secret-key"

@app.post("/api/v1/auth/login")
async def login(username: str, password: str):
    # Проверяем учетные данные
    user = authenticate_user(username, password)
    if not user:
        raise HTTPException(status_code=401, detail="Invalid credentials")
    
    # Создаём токен
    payload = {
        "sub": str(user.id),        # subject (кто)
        "username": user.username,
        "role": user.role,
        "exp": datetime.now(timezone.utc) + timedelta(hours=1),  # когда закончится
        "iat": datetime.now(timezone.utc)                         # когда создан
    }
    
    access_token = jwt.encode(payload, SECRET_KEY, algorithm="HS256")
    
    return {
        "access_token": access_token,
        "token_type": "bearer",
        "expires_in": 3600
    }

Шаг 2: Отправка токена в каждом запросе

Клиент добавляет токен в заголовок Authorization:

headers = {
    "Authorization": f"Bearer {token}",
    "Content-Type": "application/json"
}

# Все последующие запросы содержат токен
response = requests.get(
    "https://api.example.com/api/v1/users/me",
    headers=headers
)

В браузере (JavaScript/Fetch API):

const token = localStorage.getItem("token");

fetch("https://api.example.com/api/v1/users/me", {
    method: "GET",
    headers: {
        "Authorization": `Bearer ${token}`,
        "Content-Type": "application/json"
    }
})
.then(response => response.json())
.then(data => console.log(data));

Шаг 3: Валидация токена на сервере

Сервер проверяет подпись и данные:

from fastapi import Depends, HTTPException
from fastapi.security import HTTPBearer, HTTPAuthenticationCredentials

security = HTTPBearer()

async def get_current_user(credentials: HTTPAuthenticationCredentials = Depends(security)):
    token = credentials.credentials
    
    try:
        # Декодируем и проверяем подпись
        payload = jwt.decode(token, SECRET_KEY, algorithms=["HS256"])
        user_id = payload.get("sub")
        
        if not user_id:
            raise HTTPException(status_code=401, detail="Invalid token")
        
        return {"user_id": int(user_id), "role": payload.get("role")}
        
    except jwt.ExpiredSignatureError:
        raise HTTPException(status_code=401, detail="Token has expired")
    except jwt.InvalidTokenError:
        raise HTTPException(status_code=401, detail="Invalid token")

@app.get("/api/v1/users/me")
async def get_current_user_info(current_user = Depends(get_current_user)):
    user = await db.get_user(current_user["user_id"])
    return {"id": user.id, "username": user.username, "role": user.role}

Шаг 4: Обновление токена (Refresh)

Когда access токен истекает, клиент использует refresh токен:

Клиент запрашивает новый access токен:

# Когда access_token истек
if response.status_code == 401:  # Unauthorized
    refresh_token = local_storage["refresh_token"]
    
    response = requests.post(
        "https://api.example.com/api/v1/auth/refresh",
        json={"refresh_token": refresh_token}
    )
    
    if response.status_code == 200:
        new_token = response.json()["access_token"]
        local_storage["token"] = new_token
        # Повторить исходный запрос
    else:
        # Refresh токен невалиден, требуется новый login
        redirect_to_login()

Сервер выдаёт новый access токен:

@app.post("/api/v1/auth/refresh")
async def refresh_token(refresh_token: str, db: Session = Depends(get_db)):
    try:
        # Проверяем подпись refresh токена
        payload = jwt.decode(refresh_token, SECRET_KEY, algorithms=["HS256"])
        user_id = payload.get("sub")
        
        # Проверяем, что токен в БД (может быть отозван)
        token_record = db.query(RefreshToken).filter(
            RefreshToken.token == refresh_token,
            RefreshToken.user_id == user_id,
            RefreshToken.revoked == False
        ).first()
        
        if not token_record:
            raise HTTPException(status_code=401, detail="Invalid refresh token")
        
        # Создаём новый access токен
        new_payload = {
            "sub": user_id,
            "username": payload.get("username"),
            "role": payload.get("role"),
            "exp": datetime.now(timezone.utc) + timedelta(hours=1),
            "iat": datetime.now(timezone.utc)
        }
        
        new_access_token = jwt.encode(new_payload, SECRET_KEY, algorithm="HS256")
        return {"access_token": new_access_token, "token_type": "bearer"}
        
    except jwt.InvalidTokenError:
        raise HTTPException(status_code=401, detail="Invalid refresh token")

Полный цикл взаимодействия

# 1. ВХОД
POST /api/v1/auth/login
Body: {"username": "john", "password": "secret123"}
↓
Response: {
    "access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
    "refresh_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
    "token_type": "bearer",
    "expires_in": 3600
}
↓
# 2. ИСПОЛЬЗОВАНИЕ ТОКЕНА
GET /api/v1/users/me
Headers: {"Authorization": "Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."}
↓
Response: {"id": 1, "username": "john", "role": "admin"}
↓
# 3. ИСТЕЧЕНИЕ И ОБНОВЛЕНИЕ
После 1 часа access_token истекает
↓
POST /api/v1/auth/refresh
Body: {"refresh_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."}
↓
Response: {"access_token": "новый_токен", "token_type": "bearer"}
↓
# 4. ВЫХОД
POST /api/v1/auth/logout
Headers: {"Authorization": "Bearer новый_токен"}
↓
Отозвать refresh_token в БД
Ответить 200 OK

CORS и JWT

from fastapi.middleware.cors import CORSMiddleware

app.add_middleware(
    CORSMiddleware,
    allow_origins=[
        "https://frontend.example.com",
        "http://localhost:3000"
    ],
    allow_credentials=True,  # Разрешить credentials (например, cookies)
    allow_methods=["GET", "POST", "PUT", "DELETE"],
    allow_headers=["*"],  # Разрешить Authorization заголовок
    max_age=3600
)

Безопасность при передаче

⚠️ Важные правила:

  1. Всегда использовать HTTPS — токены должны передаваться по защищённому каналу
  2. Никогда не логировать токены — они содержат конфиденциальную информацию
  3. Хранить токены безопасно:
    • В браузере: httpOnly cookie или защищённое хранилище
    • На мобильной платформе: keychain/keystore
# ❌ Плохо: логировать токен
logger.info(f"User logged in with token: {token}")

# ✅ Хорошо: логировать только user_id
logger.info(f"User {user_id} logged in successfully")

Разница между Access и Refresh токенами

ПараметрAccess TokenRefresh Token
Время жизни15 минут - 1 час7-30 дней
ХранилищеMemory/localStorageSecure Storage
ИспользованиеВсе API запросыТолько для обновления
ОтзывНе требуется в БДОбязательно в БД
ПередачаВ заголовке AuthorizationВ body или cookie

Этот подход обеспечивает безопасность (короткоживущие access токены), масштабируемость (stateless проверка) и удобство (автоматическое обновление).

Как происходит клиент серверное взаимодействие с JWT? | PrepBro