[0.6.5] - 2025-09-21
Some checks failed
Deploy on push / deploy (push) Failing after 5s

### 🔐 Улучшенная аутентификация для микросервисов

####  Новые возможности
- **Универсальная аутентификация**: Добавлена функция `authenticate_request()` для всех handlers
- **Множественные источники токенов**: Поддержка Bearer, X-Session-Token, Cookie
- **Redis сессии**: Интеграция с Redis для проверки активных сессий
- **Безопасная валидация**: Функция `secure_token_validation()` с проверкой TTL и обновлением активности
- **Извлечение токенов**: Универсальная функция `extract_token_from_request()` для всех типов запросов

#### 🧪 Тестирование
- **14 новых тестов**: Полное покрытие новой логики аутентификации
- **Производительность**: Тесты производительности (< 1ms на операцию)
- **Безопасность**: Тесты защиты от подозрительных токенов
- **Граничные случаи**: Тестирование истекших токенов, неверных форматов
- **Интеграция**: Тесты с мокированным Redis

#### ♻️ Рефакторинг (DRY & YAGNI)
- **Устранение дублирования**: Объединена логика аутентификации из upload.rs и user.rs
- **Удаление устаревшего кода**: Убраны `extract_user_id_from_token`, `validate_token`, `get_user_by_token`
- **Очистка констант**: Удалены неиспользуемые `MAX_TOKEN_LENGTH`, `MIN_TOKEN_LENGTH`
- **Упрощение**: Заменена `extract_and_validate_token` на `authenticate_request`

#### ��️ Архитектурные улучшения
- **Библиотечная цель**: Добавлена `lib.rs` для тестирования модулей
- **Модульность**: Четкое разделение ответственности между модулями
- **Единообразие**: Все handlers теперь используют одинаковую логику аутентификации

#### 📋 Совместимость
- **Обратная совместимость**: Все существующие API endpoints работают без изменений
- **Graceful fallback**: Работа без Redis (JWT-only режим)
- **Множественные форматы**: Поддержка различных способов передачи токенов
This commit is contained in:
2025-09-22 01:15:35 +03:00
parent ae0fc9a18d
commit 91e5f5dac4
13 changed files with 773 additions and 355 deletions

View File

@@ -67,85 +67,132 @@ fn decode_jwt_token(token: &str) -> Result<TokenClaims, Box<dyn Error>> {
}
}
/// Быстро извлекает user_id из JWT токена для работы с квотами
pub fn extract_user_id_from_token(token: &str) -> Result<String, Box<dyn Error>> {
let claims = decode_jwt_token(token)?;
Ok(claims.user_id)
}
/// Проверяет валидность JWT токена (включая истечение срока действия)
pub fn validate_token(token: &str) -> Result<bool, Box<dyn Error>> {
match decode_jwt_token(token) {
Ok(_) => Ok(true),
Err(e) => {
warn!("Token validation failed: {}", e);
Ok(false)
/// Извлекает токен из HTTP запроса (поддерживает Bearer, X-Session-Token, Cookie)
pub fn extract_token_from_request(req: &actix_web::HttpRequest) -> Option<String> {
// 1. Bearer токен в Authorization header
if let Some(auth_header) = req.headers().get("authorization") {
if let Ok(auth_str) = auth_header.to_str() {
if let Some(stripped) = auth_str.strip_prefix("Bearer ") {
return Some(stripped.trim().to_string());
}
}
}
// 2. Кастомный заголовок X-Session-Token
if let Some(session_token) = req.headers().get("x-session-token") {
if let Ok(token_str) = session_token.to_str() {
return Some(token_str.trim().to_string());
}
}
// 3. Cookie session_token (для веб-приложений)
if let Some(cookie_header) = req.headers().get("cookie") {
if let Ok(cookie_str) = cookie_header.to_str() {
for cookie in cookie_str.split(';') {
let cookie = cookie.trim();
if let Some(stripped) = cookie.strip_prefix("session_token=") {
return Some(stripped.to_string());
}
}
}
}
None
}
/// Получает user_id из JWT токена и базовые данные пользователя с таймаутом
pub async fn get_user_by_token(
/// Безопасная валидация токена с проверкой Redis сессий
pub async fn secure_token_validation(
token: &str,
mut redis: Option<&mut MultiplexedConnection>,
timeout: Duration,
) -> Result<Author, Box<dyn Error>> {
// Декодируем JWT токен для получения user_id
// Базовая проверка формата токена
if token.is_empty() || token.len() < 10 {
return Err(Box::new(std::io::Error::other("Invalid token format")));
}
// 1. Проверяем JWT структуру и подпись
let claims = decode_jwt_token(token)?;
let user_id = &claims.user_id;
info!("Extracted user_id from JWT token: {}", user_id);
info!("JWT token validated for user: {}", user_id);
// Проверяем валидность токена через сессию в Redis (опционально) с таймаутом
// 2. Проверяем существование сессии в Redis
let session_exists = if let Some(ref mut redis) = redis {
let token_key = format!("session:{}:{}", user_id, token);
tokio::time::timeout(timeout, redis.exists(&token_key))
.await
.map_err(|_| {
warn!("Redis timeout checking session existence");
// Не критичная ошибка, продолжаем с базовыми данными
})
.unwrap_or(Ok(false))
.map_err(|e| {
warn!("Failed to check session existence in Redis: {}", e);
// Не критичная ошибка, продолжаем с базовыми данными
})
.unwrap_or(false)
let session_key = format!("session:{}:{}", user_id, token);
match tokio::time::timeout(timeout, redis.exists(&session_key)).await {
Ok(Ok(exists)) => exists,
Ok(Err(e)) => {
warn!("Redis error checking session: {}", e);
false
}
Err(_) => {
warn!("Redis timeout checking session");
false
}
}
} else {
warn!("⚠️ Redis not available, skipping session validation");
false
};
if session_exists {
// Обновляем last_activity если сессия существует
if let Some(redis) = redis {
let current_time = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap()
.as_secs();
if !session_exists {
info!("Session not found in Redis for user: {}", user_id);
// В соответствии с руководством, можем продолжить с JWT-only данными
// или вернуть ошибку в зависимости от политики безопасности
}
let token_key = format!("session:{}:{}", user_id, token);
let _: () = tokio::time::timeout(
// 3. Проверяем TTL сессии если она существует
let ttl = if session_exists && redis.is_some() {
let session_key = format!("session:{}:{}", user_id, token);
if let Some(ref mut redis) = redis {
match tokio::time::timeout(timeout, redis.ttl(&session_key)).await {
Ok(Ok(ttl_value)) => {
if ttl_value <= 0 {
return Err(Box::new(std::io::Error::other("Session expired")));
}
ttl_value
}
Ok(Err(e)) => {
warn!("Redis error getting TTL: {}", e);
-1
}
Err(_) => {
warn!("Redis timeout getting TTL");
-1
}
}
} else {
-1
}
} else {
-1
};
// 4. Обновляем last_activity если сессия активна
if session_exists && redis.is_some() {
let current_time = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap()
.as_secs();
let session_key = format!("session:{}:{}", user_id, token);
if let Some(redis) = redis {
let _: Result<(), _> = tokio::time::timeout(
timeout,
redis.hset(&token_key, "last_activity", current_time.to_string()),
redis.hset(&session_key, "last_activity", current_time.to_string()),
)
.await
.map_err(|_| {
warn!("Redis timeout updating last_activity");
})
.map_err(|_| warn!("Redis timeout updating last_activity"))
.unwrap_or(Ok(()))
.map_err(|e| {
warn!("Failed to update last_activity: {}", e);
})
.unwrap_or(());
.map_err(|e| warn!("Failed to update last_activity: {}", e));
}
info!("Updated last_activity for session: {}", user_id);
} else {
info!("Session not found in Redis, proceeding with JWT-only data");
}
// Создаем базовый объект Author с данными из JWT
// Создаем объект Author с расширенными данными
let author = Author {
user_id: user_id.clone(),
username: claims.username.clone(),
@@ -158,14 +205,43 @@ pub async fn get_user_by_token(
.as_secs()
.to_string(),
),
auth_data: None,
auth_data: if session_exists {
Some(format!("redis_session_ttl:{}", ttl))
} else {
Some("jwt_only".to_string())
},
device_info: None,
};
info!("Successfully created author data for user_id: {}", user_id);
info!(
"Successfully validated token for user: {} (session_exists: {})",
user_id, session_exists
);
Ok(author)
}
/// Универсальная функция аутентификации для всех handlers
/// Извлекает токен из запроса и выполняет полную валидацию
pub async fn authenticate_request(
req: &actix_web::HttpRequest,
redis: Option<&mut MultiplexedConnection>,
timeout: Duration,
) -> Result<Author, actix_web::Error> {
// Извлекаем токен из запроса (поддерживает Bearer, X-Session-Token, Cookie)
let token = extract_token_from_request(req).ok_or_else(|| {
warn!("No authorization token provided");
actix_web::error::ErrorUnauthorized("Authorization token required")
})?;
// Безопасная валидация токена с проверкой Redis сессий
secure_token_validation(&token, redis, timeout)
.await
.map_err(|e| {
warn!("Token validation failed: {}", e);
actix_web::error::ErrorUnauthorized("Invalid or expired token")
})
}
/// Сохраняет имя файла в Redis для пользователя
pub async fn user_added_file(
redis: Option<&mut MultiplexedConnection>,