Files
core/docs/auth/sessions.md

503 lines
15 KiB
Markdown
Raw Normal View History

# 🔑 Управление сессиями
## 🎯 Обзор
Система управления сессиями на основе JWT токенов с Redis хранением для отзыва и мониторинга активности.
## 🏗️ Архитектура
### Принцип работы
1. **JWT токены** с payload `{user_id, username, iat, exp}`
2. **Redis хранение** для отзыва и управления жизненным циклом
3. **Множественные сессии** на пользователя
4. **Автоматическое обновление** `last_activity` при активности
### Redis структура
```bash
session:{user_id}:{token} # Hash: {user_id, username, device_info, last_activity}
user_sessions:{user_id} # Set: {token1, token2, ...}
```
### Извлечение токена (приоритет)
1. Cookie `session_token` (httpOnly)
2. Заголовок `Authorization: Bearer <token>`
3. Заголовок `X-Session-Token`
4. `scope["auth_token"]` (внутренний)
## 🔧 SessionTokenManager
### Основные методы
```python
from auth.tokens.sessions import SessionTokenManager
sessions = SessionTokenManager()
# Создание сессии
token = await sessions.create_session(
user_id="123",
auth_data={"provider": "local"},
username="john_doe",
device_info={"ip": "192.168.1.1", "user_agent": "Mozilla/5.0"}
)
# Создание JWT токена сессии
token = await sessions.create_session_token(
user_id="123",
token_data={"username": "john_doe", "device_info": "..."}
)
# Проверка сессии
payload = await sessions.verify_session(token)
# Возвращает: {"user_id": "123", "username": "john_doe", "iat": 1640995200, "exp": 1643587200}
# Валидация токена сессии
valid, data = await sessions.validate_session_token(token)
# Получение данных сессии
session_data = await sessions.get_session_data(token, user_id)
# Обновление сессии
new_token = await sessions.refresh_session(user_id, old_token, device_info)
# Отзыв сессии
await sessions.revoke_session_token(token)
# Отзыв всех сессий пользователя
revoked_count = await sessions.revoke_user_sessions(user_id)
# Получение всех сессий пользователя
user_sessions = await sessions.get_user_sessions(user_id)
```
## 🍪 httpOnly Cookies
### Принципы работы
1. **Безопасное хранение**: Токены сессий хранятся в httpOnly cookies, недоступных для JavaScript
2. **Автоматическая отправка**: Cookies автоматически отправляются с каждым запросом
3. **Защита от XSS**: httpOnly cookies защищены от кражи через JavaScript
4. **Двойная поддержка**: Система поддерживает как cookies, так и заголовок Authorization
### Конфигурация cookies
```python
# settings.py
SESSION_COOKIE_NAME = "session_token"
SESSION_COOKIE_HTTPONLY = True
SESSION_COOKIE_SECURE = True # для HTTPS
SESSION_COOKIE_SAMESITE = "lax"
SESSION_COOKIE_MAX_AGE = 30 * 24 * 60 * 60 # 30 дней
```
### Установка cookies
```python
# В AuthMiddleware
def set_session_cookie(self, response: Response, token: str) -> None:
"""Устанавливает httpOnly cookie с токеном сессии"""
response.set_cookie(
key=SESSION_COOKIE_NAME,
value=token,
httponly=SESSION_COOKIE_HTTPONLY,
secure=SESSION_COOKIE_SECURE,
samesite=SESSION_COOKIE_SAMESITE,
max_age=SESSION_COOKIE_MAX_AGE
)
```
## 🔍 Извлечение токенов
### Автоматическое извлечение
```python
from auth.utils import extract_token_from_request, get_auth_token, get_safe_headers
# Простое извлечение из cookies/headers
token = await extract_token_from_request(request)
# Расширенное извлечение с логированием
token = await get_auth_token(request)
# Ручная проверка источников
headers = get_safe_headers(request)
token = headers.get("authorization", "").replace("Bearer ", "")
# Извлечение из GraphQL контекста
from auth.utils import get_auth_token_from_context
token = await get_auth_token_from_context(info)
```
### Приоритет источников
Система проверяет токены в следующем порядке приоритета:
1. **httpOnly cookies** - основной источник для веб-приложений
2. **Заголовок Authorization** - для API клиентов и мобильных приложений
```python
# auth/utils.py
async def extract_token_from_request(request) -> str | None:
"""DRY функция для извлечения токена из request"""
# 1. Проверяем cookies
if hasattr(request, "cookies") and request.cookies:
token = request.cookies.get(SESSION_COOKIE_NAME)
if token:
return token
# 2. Проверяем заголовок Authorization
headers = get_safe_headers(request)
auth_header = headers.get("authorization", "")
if auth_header and auth_header.startswith("Bearer "):
token = auth_header[7:].strip()
return token
return None
```
### Безопасное получение заголовков
```python
# auth/utils.py
def get_safe_headers(request: Any) -> dict[str, str]:
"""Безопасно получает заголовки запроса"""
headers = {}
try:
# Первый приоритет: scope из ASGI
if hasattr(request, "scope") and isinstance(request.scope, dict):
scope_headers = request.scope.get("headers", [])
if scope_headers:
headers.update({k.decode("utf-8").lower(): v.decode("utf-8")
for k, v in scope_headers})
# Второй приоритет: метод headers() или атрибут headers
if hasattr(request, "headers"):
if callable(request.headers):
h = request.headers()
if h:
headers.update({k.lower(): v for k, v in h.items()})
else:
h = request.headers
if hasattr(h, "items") and callable(h.items):
headers.update({k.lower(): v for k, v in h.items()})
except Exception as e:
logger.warning(f"Ошибка при доступе к заголовкам: {e}")
return headers
```
## 🔄 Жизненный цикл сессии
### Создание сессии
```python
# auth/tokens/sessions.py
async def create_session(author_id: int, email: str, **kwargs) -> str:
"""Создает новую сессию для пользователя"""
session_data = {
"author_id": author_id,
"email": email,
"created_at": int(time.time()),
**kwargs
}
# Генерируем уникальный токен
token = generate_session_token()
# Сохраняем в Redis
await redis.execute(
"SETEX",
f"session:{token}",
SESSION_TOKEN_LIFE_SPAN,
json.dumps(session_data)
)
return token
```
### Верификация сессии
```python
# auth/tokens/storage.py
async def verify_session(token: str) -> dict | None:
"""Верифицирует токен сессии"""
if not token:
return None
try:
# Получаем данные сессии из Redis
session_data = await redis.execute("GET", f"session:{token}")
if not session_data:
return None
return json.loads(session_data)
except Exception as e:
logger.error(f"Ошибка верификации сессии: {e}")
return None
```
### Обновление сессии
```python
async def refresh_session(user_id: str, old_token: str, device_info: dict = None) -> str:
"""Обновляет сессию пользователя"""
# Проверяем старую сессию
old_payload = await verify_session(old_token)
if not old_payload:
raise InvalidTokenError("Invalid session token")
# Отзываем старый токен
await revoke_session_token(old_token)
# Создаем новый токен
new_token = await create_session(
user_id=user_id,
username=old_payload.get("username"),
device_info=device_info or old_payload.get("device_info", {})
)
return new_token
```
### Удаление сессии
```python
# auth/tokens/storage.py
async def delete_session(token: str) -> bool:
"""Удаляет сессию пользователя"""
try:
result = await redis.execute("DEL", f"session:{token}")
return bool(result)
except Exception as e:
logger.error(f"Ошибка удаления сессии: {e}")
return False
```
## 🔒 Безопасность
### JWT токены
- **Алгоритм**: HS256
2025-09-30 21:48:29 +03:00
- **Secret**: Из переменной окружения JWT_SECRET_KEY
- **Payload**: `{user_id, username, iat, exp}`
- **Expiration**: 30 дней (настраивается)
### Redis security
- **TTL** для всех токенов
- **Атомарные операции** через pipelines
- **SCAN** вместо KEYS для производительности
- **Транзакции** для критических операций
### Защита от атак
- **XSS**: httpOnly cookies недоступны для JavaScript
- **CSRF**: SameSite cookies и CSRF токены
- **Session Hijacking**: Secure cookies и регулярная ротация токенов
- **Brute Force**: Ограничение попыток входа и блокировка аккаунтов
## 📊 Мониторинг сессий
### Статистика
```python
from auth.tokens.monitoring import TokenMonitoring
monitoring = TokenMonitoring()
# Статистика токенов
stats = await monitoring.get_token_statistics()
print(f"Active sessions: {stats['session_tokens']}")
print(f"Memory usage: {stats['memory_usage']} bytes")
# Health check
health = await monitoring.health_check()
if health["status"] == "healthy":
print("Session system is healthy")
```
### Логирование событий
```python
# auth/middleware.py
def log_auth_event(event_type: str, user_id: int | None = None,
success: bool = True, **kwargs):
"""Логирует события авторизации"""
logger.info(
"auth_event",
event_type=event_type,
user_id=user_id,
success=success,
ip_address=kwargs.get('ip'),
user_agent=kwargs.get('user_agent'),
**kwargs
)
```
### Метрики
```python
# auth/middleware.py
from prometheus_client import Counter, Histogram
# Счетчики
login_attempts = Counter('auth_login_attempts_total', 'Number of login attempts', ['success'])
session_creations = Counter('auth_sessions_created_total', 'Number of sessions created')
session_deletions = Counter('auth_sessions_deleted_total', 'Number of sessions deleted')
# Гистограммы
auth_duration = Histogram('auth_operation_duration_seconds', 'Time spent on auth operations', ['operation'])
```
## 🧪 Тестирование
### Unit тесты
```python
import pytest
from httpx import AsyncClient
@pytest.mark.asyncio
async def test_login_success(client: AsyncClient):
"""Тест успешного входа"""
response = await client.post("/auth/login", json={
"email": "test@example.com",
"password": "password123"
})
assert response.status_code == 200
data = response.json()
assert data["success"] is True
assert "token" in data
# Проверяем установку cookie
cookies = response.cookies
assert "session_token" in cookies
@pytest.mark.asyncio
async def test_protected_endpoint_with_cookie(client: AsyncClient):
"""Тест защищенного endpoint с cookie"""
# Сначала входим в систему
login_response = await client.post("/auth/login", json={
"email": "test@example.com",
"password": "password123"
})
# Получаем cookie
session_cookie = login_response.cookies.get("session_token")
# Делаем запрос к защищенному endpoint
response = await client.get("/auth/session", cookies={
"session_token": session_cookie
})
assert response.status_code == 200
data = response.json()
assert data["user"]["email"] == "test@example.com"
```
## 💡 Примеры использования
### 1. Вход в систему
```typescript
// Frontend - React/SolidJS
const handleLogin = async (email: string, password: string) => {
try {
const response = await fetch('/auth/login', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ email, password }),
credentials: 'include', // Важно для cookies
});
if (response.ok) {
const data = await response.json();
// Cookie автоматически установится браузером
// Перенаправляем на главную страницу
window.location.href = '/';
} else {
const error = await response.json();
console.error('Login failed:', error.message);
}
} catch (error) {
console.error('Login error:', error);
}
};
```
### 2. Проверка авторизации
```typescript
// Frontend - проверка текущей сессии
const checkAuth = async () => {
try {
const response = await fetch('/auth/session', {
credentials: 'include',
});
if (response.ok) {
const data = await response.json();
if (data.user) {
// Пользователь авторизован
setUser(data.user);
setIsAuthenticated(true);
}
}
} catch (error) {
console.error('Auth check failed:', error);
}
};
```
### 3. Защищенный API endpoint
```python
# Backend - Python
from auth.decorators import login_required, require_permission
@login_required
@require_permission("shout:create")
async def create_shout(info, input_data):
"""Создание публикации с проверкой прав"""
user = info.context.get('user')
# Создаем публикацию
shout = Shout(
title=input_data['title'],
content=input_data['content'],
author_id=user.id
)
db.add(shout)
db.commit()
return shout
```
### 4. Выход из системы
```typescript
// Frontend - выход
const handleLogout = async () => {
try {
await fetch('/auth/logout', {
method: 'POST',
credentials: 'include',
});
// Очищаем локальное состояние
setUser(null);
setIsAuthenticated(false);
// Перенаправляем на страницу входа
window.location.href = '/login';
} catch (error) {
console.error('Logout failed:', error);
}
};
```