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

Зачем нужно использовать 2 токена для авторизации?

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

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

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

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

Двухтокенная авторизация (Access + Refresh tokens): зачем нужна

Двухтокенная система с Access Token и Refresh Token — это стандартный паттерн для безопасной авторизации в современных веб-приложениях. За 10+ лет я видел, что правильное использование этого паттерна предотвращает множество проблем безопасности.

Проблема: один токен на всю жизнь (ПЛОХО)

# Наивный подход: один JWT токен на месяцы
from datetime import datetime, timedelta
import jwt

token = jwt.encode(
    {
        'user_id': 123,
        'exp': datetime.utcnow() + timedelta(days=30)  # Живёт 30 дней!
    },
    'secret-key',
    algorithm='HS256'
)

# Проблемы этого подхода:
# 1. Если токен украдут (например, с клиента) — у злоумышленника доступ на 30 дней
# 2. Нельзя срочно отозвать токен (даже если узнали о компрометации)
# 3. Длинный срок жизни = больший риск

Решение: два токена

Access Token (SHORT-LIVED)        Refresh Token (LONG-LIVED)
├─ Живёт 15 минут                 ├─ Живёт 7 дней
├─ Хранится в памяти/sessionStorage│─ Хранится в httpOnly cookie
├─ Отправляется в каждом запросе  │─ Отправляется только для обновления
└─ Если украдут — малый урон      └─ Более защищён (httpOnly cookie)

Архитектура двухтокенной системы

from fastapi import FastAPI, Depends, HTTPException, status
from datetime import datetime, timedelta
from typing import Optional
import jwt
from pydantic import BaseModel

app = FastAPI()

SECRET_KEY = "your-secret-key"
ACCESS_TOKEN_EXPIRE_MINUTES = 15  # Короткий срок
REFRESH_TOKEN_EXPIRE_DAYS = 7     # Длинный срок

class Token(BaseModel):
    access_token: str
    refresh_token: str
    token_type: str = "bearer"

class TokenData(BaseModel):
    user_id: int
    type: str  # "access" или "refresh"

# Шаг 1: Функция создания токенов
def create_tokens(user_id: int):
    """Создаёт оба токена"""
    
    # Access token — короткоживущий
    access_payload = {
        'user_id': user_id,
        'type': 'access',
        'exp': datetime.utcnow() + timedelta(minutes=ACCESS_TOKEN_EXPIRE_MINUTES)
    }
    access_token = jwt.encode(
        access_payload,
        SECRET_KEY,
        algorithm='HS256'
    )
    
    # Refresh token — долгоживущий
    refresh_payload = {
        'user_id': user_id,
        'type': 'refresh',
        'exp': datetime.utcnow() + timedelta(days=REFRESH_TOKEN_EXPIRE_DAYS)
    }
    refresh_token = jwt.encode(
        refresh_payload,
        SECRET_KEY,
        algorithm='HS256'
    )
    
    return Token(
        access_token=access_token,
        refresh_token=refresh_token
    )

# Шаг 2: Логин — выдаём оба токена
@app.post("/login")
def login(username: str, password: str):
    """Пользователь вводит логин/пароль"""
    # Проверяем в БД
    user = verify_credentials(username, password)  # Твоя функция
    
    if not user:
        raise HTTPException(
            status_code=status.HTTP_401_UNAUTHORIZED,
            detail="Invalid credentials"
        )
    
    tokens = create_tokens(user.id)
    
    # Access token отправляем в body (клиент хранит в памяти)
    # Refresh token отправляем в httpOnly cookie (безопаснее)
    response = {
        'access_token': tokens.access_token,
        'token_type': 'bearer'
    }
    
    # Refresh token в httpOnly cookie (не доступен JS!)
    # response.set_cookie(
    #     key='refresh_token',
    #     value=tokens.refresh_token,
    #     httponly=True,
    #     secure=True,  # Только HTTPS
    #     samesite='strict'
    # )
    
    return response

# Шаг 3: Использование Access Token в каждом запросе
def verify_access_token(token: str):
    """Проверяет Access token"""
    try:
        payload = jwt.decode(
            token,
            SECRET_KEY,
            algorithms=['HS256']
        )
        
        # Проверяем тип токена
        if payload.get('type') != 'access':
            raise HTTPException(
                status_code=status.HTTP_401_UNAUTHORIZED,
                detail="Invalid token type"
            )
        
        return payload['user_id']
    
    except jwt.ExpiredSignatureError:
        raise HTTPException(
            status_code=status.HTTP_401_UNAUTHORIZED,
            detail="Access token expired"
        )
    except jwt.InvalidTokenError:
        raise HTTPException(
            status_code=status.HTTP_401_UNAUTHORIZED,
            detail="Invalid token"
        )

@app.get("/me")
def get_current_user(authorization: str = Depends(verify_access_token)):
    """Защищённый эндпоинт"""
    return {"user_id": authorization}

# Шаг 4: Обновление Access Token с помощью Refresh Token
@app.post("/refresh")
def refresh_access_token(refresh_token: str):
    """Клиент отправляет Refresh Token, получает новый Access Token"""
    try:
        payload = jwt.decode(
            refresh_token,
            SECRET_KEY,
            algorithms=['HS256']
        )
        
        # Проверяем тип токена
        if payload.get('type') != 'refresh':
            raise HTTPException(
                status_code=status.HTTP_401_UNAUTHORIZED,
                detail="Invalid token type"
            )
        
        user_id = payload['user_id']
        
        # Создаём новый Access Token
        new_access_payload = {
            'user_id': user_id,
            'type': 'access',
            'exp': datetime.utcnow() + timedelta(minutes=ACCESS_TOKEN_EXPIRE_MINUTES)
        }
        new_access_token = jwt.encode(
            new_access_payload,
            SECRET_KEY,
            algorithm='HS256'
        )
        
        return {
            'access_token': new_access_token,
            'token_type': 'bearer'
        }
    
    except jwt.ExpiredSignatureError:
        raise HTTPException(
            status_code=status.HTTP_401_UNAUTHORIZED,
            detail="Refresh token expired. Please login again."
        )
    except jwt.InvalidTokenError:
        raise HTTPException(
            status_code=status.HTTP_401_UNAUTHORIZED,
            detail="Invalid refresh token"
        )

Поток работы (UX)

1. Пользователь заходит на сайт
   ↓
2. Нажимает "Login"
   ↓
3. POST /login (username=alice, password=***)
   ↓
4. Сервер проверяет — OK!
   ← Возвращает: {
       "access_token": "eyJ0eXAi...",
       "refresh_token": "eyJ0eXAi..." (в httpOnly cookie)
     }
   ↓
5. Клиент сохраняет:
   - access_token в памяти (sessionStorage)
   - refresh_token автоматически в cookie
   ↓
6. Каждый запрос к API отправляет Access Token:
   GET /me
   Authorization: Bearer eyJ0eXAi...
   ↓
7. Через 15 минут Access Token истекает
   ↓
8. Клиент автоматически отправляет Refresh Token:
   POST /refresh
   refresh_token: eyJ0eXAi...
   ↓
9. Сервер выдаёт новый Access Token
   ← {"access_token": "eyJ0eXAi..."}
   ↓
10. Клиент обновляет Access Token в памяти
    Жизнь продолжается!

Почему двухтокенная система безопаснее

ПроблемаОдиночный токенДвухтокенная система
Украден Access TokenДоступ на месяцыДоступ на 15 минут только
Украден Refresh TokenНет проблемы (его нет)Доступ на 7 дней, но можно отозвать
Срочное отзывНевозможно (token живой)Легко — удаляем из БД
Фишинг/компрометацияКатастрофа на месяцыОграниченный урон на 15 минут
Безопасность в памятиВесь токен уязвимAccess в памяти (меньший риск)

Хранение токенов на клиенте

// ❌ ПЛОХО: localStorage (уязвим для XSS)
localStorage.setItem('access_token', token);
const token = localStorage.getItem('access_token');
// Если на сайте XSS — злоумышленник может прочитать токен

// ✅ ХОРОШО: sessionStorage (чуть безопаснее, но всё ещё уязвим)
sessionStorage.setItem('access_token', token);

// ✅ ЛУЧШЕ: память (переменная)
let accessToken = token;
// Живёт только в памяти, теряется при перезагрузке (нужен refresh)

// ✅ ЛУЧШЕ: httpOnly cookie (защищено от XSS!)
// Сервер сам устанавливает cookie
response.set_cookie(
    key='refresh_token',
    value=refresh_token,
    httponly=True,  # JavaScript не может прочитать!
    secure=True,    # Только HTTPS
    samesite='strict'  # Защита от CSRF
)

Логаут с двухтокенной системой

@app.post("/logout")
def logout():
    """Logout: удаляем Refresh Token"""
    response = {
        'message': 'Logged out successfully'
    }
    
    # Удаляем refresh_token cookie
    # response.delete_cookie('refresh_token')
    
    # Опционально: добавляем Access Token в blacklist
    # (в Redis или БД на короткое время)
    # blacklist.add(current_access_token, expiration=15*60)
    
    return response

Когда использовать двухтокенную систему

✅ Используй всегда для:

  • Веб-приложения с аутентификацией
  • Мобильные приложения
  • REST API
  • Когда безопасность важна

❌ Когда можно обойтись:

  • Session cookies (Django по умолчанию)
  • Для внутренних микросервисов (используй mTLS вместо токенов)

Заключение

Двухтокенная система (Access + Refresh) — это золотой стандарт для авторизации потому что:

  1. Краткий срок Access Token — даже если украдут, урон ограничен 15 минутами
  2. Refresh Token в httpOnly cookie — защищён от XSS
  3. Возможность отзыва — можем срочно отозвать токен
  4. Лучший UX — пользователь не должен заново логиниться каждые 15 минут
  5. Масштабируемость — работает с микросервисной архитектурой

Это не просто паттерн — это стандарт индустрии, используемый Google, Amazon, GitHub и везде где важна безопасность.

Зачем нужно использовать 2 токена для авторизации? | PrepBro