← Назад к вопросам
Зачем нужно использовать 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) — это золотой стандарт для авторизации потому что:
- Краткий срок Access Token — даже если украдут, урон ограничен 15 минутами
- Refresh Token в httpOnly cookie — защищён от XSS
- Возможность отзыва — можем срочно отозвать токен
- Лучший UX — пользователь не должен заново логиниться каждые 15 минут
- Масштабируемость — работает с микросервисной архитектурой
Это не просто паттерн — это стандарт индустрии, используемый Google, Amazon, GitHub и везде где важна безопасность.