Аутентификация в Axum
Аутентификация - критически важная часть большинства веб-приложений. Axum предоставляет гибкие механизмы для реализации различных стратегий аутентификации. В этом разделе рассмотрим основные подходы к реализации аутентификации в приложениях на базе Axum.
Содержание
- Основные концепции
- Реализация JWT-аутентификации
- Сессионная аутентификация
- OAuth и сторонние провайдеры
- Базовая HTTP-аутентификация
- Интеграция с Middleware
- Лучшие практики
Основные концепции
В Axum аутентификация обычно реализуется через:
- Экстракторы - для получения и валидации учетных данных из запроса
- Middleware - для проверки аутентификации перед обработкой запроса
- TypedHeader - для работы с заголовками аутентификации
- Кастомные типы - для представления аутентифицированного пользователя
Реализация JWT-аутентификации
JSON Web Token (JWT) - популярный метод аутентификации для REST API.
Настройка зависимостей
[dependencies]
axum = "0.7.2"
jsonwebtoken = "9.1.0"
serde = { version = "1.0", features = ["derive"] }
time = "0.3"
Определение типов данных
use axum::{
async_trait,
extract::{FromRequest, RequestParts, TypedHeader},
headers::{authorization::Bearer, Authorization},
http::StatusCode,
response::IntoResponse,
Json,
};
use jsonwebtoken::{decode, encode, DecodingKey, EncodingKey, Header, Validation};
use serde::{Deserialize, Serialize};
use std::time::{Duration, SystemTime, UNIX_EPOCH};
// Определение структуры данных для хранения информации о пользователе в токене
#[derive(Debug, Serialize, Deserialize)]
struct Claims {
sub: String, // идентификатор пользователя
exp: usize, // timestamp истечения срока действия
name: String, // имя пользователя
role: String, // роль пользователя
}
// Структура, представляющая аутентифицированного пользователя
#[derive(Debug)]
struct AuthUser {
user_id: String,
name: String,
role: String,
}
Создание экстрактора для JWT
#[async_trait]
impl<B> FromRequest<B> for AuthUser
where
B: Send,
{
type Rejection = (StatusCode, String);
async fn from_request(req: &mut RequestParts<B>) -> Result<Self, Self::Rejection> {
// Получение заголовка авторизации
let TypedHeader(Authorization(bearer)) =
TypedHeader::<Authorization<Bearer>>::from_request(req)
.await
.map_err(|_| {
(StatusCode::UNAUTHORIZED, "Отсутствует заголовок авторизации".to_string())
})?;
// Декодирование и проверка JWT
let token_data = decode::<Claims>(
bearer.token(),
&DecodingKey::from_secret("мой_секретный_ключ".as_bytes()),
&Validation::default(),
)
.map_err(|_| (StatusCode::UNAUTHORIZED, "Некорректный токен".to_string()))?;
// Проверка срока действия токена
let claims = token_data.claims;
let current_time = SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap()
.as_secs() as usize;
if claims.exp < current_time {
return Err((StatusCode::UNAUTHORIZED, "Токен истек".to_string()));
}
// Создание объекта пользователя
Ok(AuthUser {
user_id: claims.sub,
name: claims.name,
role: claims.role,
})
}
}
Реализация аутентификации
// Структура для запроса авторизации
#[derive(Debug, Deserialize)]
struct LoginRequest {
username: String,
password: String,
}
// Структура для ответа с токеном
#[derive(Debug, Serialize)]
struct LoginResponse {
access_token: String,
token_type: String,
expires_in: u64,
}
// Обработчик для логина
async fn login(Json(payload): Json<LoginRequest>) -> Result<impl IntoResponse, StatusCode> {
// В реальном приложении здесь будет проверка в БД
if payload.username == "admin" && payload.password == "password" {
let expires_in = 3600; // срок действия токена в секундах
// Текущее время и срок истечения
let now = SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap()
.as_secs() as usize;
let exp = now + expires_in as usize;
// Создание claims
let claims = Claims {
sub: "1".to_string(),
exp,
name: payload.username,
role: "admin".to_string(),
};
// Генерация JWT
let token = encode(
&Header::default(),
&claims,
&EncodingKey::from_secret("мой_секретный_ключ".as_bytes()),
)
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
// Формирование ответа
let response = LoginResponse {
access_token: token,
token_type: "Bearer".to_string(),
expires_in,
};
Ok(Json(response))
} else {
Err(StatusCode::UNAUTHORIZED)
}
}
// Защищенный маршрут, требующий аутентификации
async fn protected_route(auth_user: AuthUser) -> impl IntoResponse {
format!("Привет, {}! Ваша роль: {}", auth_user.name, auth_user.role)
}
// Настройка маршрутов
fn routes() -> Router {
Router::new()
.route("/login", post(login))
.route("/protected", get(protected_route))
}
Сессионная аутентификация
Для приложений, использующих сессии, можно реализовать сессионную аутентификацию:
use axum::{
extract::{FromRef, State},
http::{header, StatusCode},
response::{IntoResponse, Response},
};
use axum_extra::extract::{cookie::{Cookie, SameSite}, CookieJar};
use serde::{Deserialize, Serialize};
use std::sync::{Arc, RwLock};
use time::{Duration, OffsetDateTime};
use uuid::Uuid;
// Структура сессии
#[derive(Debug, Clone, Serialize, Deserialize)]
struct Session {
id: String,
user_id: String,
username: String,
expiry: OffsetDateTime,
}
// Хранилище сессий (в реальном приложении это будет Redis или БД)
#[derive(Clone)]
struct SessionStore {
sessions: Arc<RwLock<Vec<Session>>>,
}
impl SessionStore {
fn new() -> Self {
Self {
sessions: Arc::new(RwLock::new(Vec::new())),
}
}
fn create_session(&self, user_id: String, username: String) -> Session {
let id = Uuid::new_v4().to_string();
let expiry = OffsetDateTime::now_utc() + Duration::hours(24);
let session = Session {
id,
user_id,
username,
expiry,
};
// Сохранение сессии
self.sessions.write().unwrap().push(session.clone());
session
}
fn get_session(&self, id: &str) -> Option<Session> {
let sessions = self.sessions.read().unwrap();
sessions.iter()
.find(|s| s.id == id && s.expiry > OffsetDateTime::now_utc())
.cloned()
}
fn delete_session(&self, id: &str) {
let mut sessions = self.sessions.write().unwrap();
sessions.retain(|s| s.id != id);
}
}
// Обработчик для логина с сессией
async fn login_with_session(
State(session_store): State<SessionStore>,
jar: CookieJar,
Json(payload): Json<LoginRequest>,
) -> Result<impl IntoResponse, StatusCode> {
// Проверка учетных данных
if payload.username == "admin" && payload.password == "password" {
// Создание сессии
let session = session_store.create_session("1".to_string(), payload.username);
// Установка куки
let cookie = Cookie::build(("session_id", session.id))
.path("/")
.secure(true)
.http_only(true)
.same_site(SameSite::Lax)
.expires(session.expiry)
.build();
// Возврат ответа с кукой
Ok((jar.add(cookie), "Успешный вход"))
} else {
Err(StatusCode::UNAUTHORIZED)
}
}
// Экстрактор для текущего пользователя
#[async_trait]
impl<B> FromRequest<B> for AuthUser
where
B: Send,
SessionStore: FromRef<SessionStore>,
{
type Rejection = StatusCode;
async fn from_request(req: &mut RequestParts<B>) -> Result<Self, Self::Rejection> {
// Получение куки
let jar = CookieJar::from_request(req).await
.map_err(|_| StatusCode::UNAUTHORIZED)?;
let session_id = jar.get("session_id")
.ok_or(StatusCode::UNAUTHORIZED)?
.value();
// Получение хранилища сессий
let session_store = SessionStore::from_ref(&req.extensions().get::<SessionStore>()
.ok_or(StatusCode::INTERNAL_SERVER_ERROR)?);
// Получение сессии по ID
let session = session_store.get_session(session_id)
.ok_or(StatusCode::UNAUTHORIZED)?;
// Создание пользователя из сессии
Ok(AuthUser {
user_id: session.user_id,
name: session.username,
role: "user".to_string(), // В реальном приложении роль хранится в сессии
})
}
}
// Обработчик для выхода
async fn logout(
State(session_store): State<SessionStore>,
jar: CookieJar,
) -> impl IntoResponse {
if let Some(cookie) = jar.get("session_id") {
session_store.delete_session(cookie.value());
// Удаление куки
let removed_cookie = Cookie::build("session_id")
.path("/")
.max_age(time::Duration::seconds(-1))
.build();
return (jar.remove(removed_cookie), "Успешный выход");
}
(jar, "Сессия не найдена")
}
OAuth и сторонние провайдеры
Для интеграции с OAuth провайдерами (Google, GitHub и др.) можно использовать библиотеку oauth2
:
use axum::{
extract::{Query, State},
response::{IntoResponse, Redirect},
routing::get,
Router,
};
use oauth2::{
basic::BasicClient,
reqwest::async_http_client,
AuthUrl, AuthorizationCode, ClientId, ClientSecret, CsrfToken, RedirectUrl,
Scope, TokenResponse, TokenUrl,
};
use serde::Deserialize;
use std::sync::Arc;
// Структура для провайдера OAuth
struct OAuthClient {
client: BasicClient,
}
// Параметры запроса для callback
#[derive(Debug, Deserialize)]
struct AuthRequest {
code: String,
state: String,
}
// Создание клиента OAuth для GitHub
fn create_github_client() -> OAuthClient {
let client = BasicClient::new(
ClientId::new("client_id".to_string()),
Some(ClientSecret::new("client_secret".to_string())),
AuthUrl::new("https://github.com/login/oauth/authorize".to_string()).unwrap(),
Some(TokenUrl::new("https://github.com/login/oauth/access_token".to_string()).unwrap()),
)
.set_redirect_uri(RedirectUrl::new("http://localhost:3000/auth/github/callback".to_string()).unwrap());
OAuthClient { client }
}
// Обработчик для начала процесса аутентификации
async fn github_auth(State(oauth_client): State<Arc<OAuthClient>>) -> impl IntoResponse {
// Генерация URL для аутентификации
let (auth_url, _csrf_token) = oauth_client
.client
.authorize_url(CsrfToken::new_random)
.add_scope(Scope::new("user:email".to_string()))
.url();
// Редирект на страницу авторизации GitHub
Redirect::to(auth_url.as_str())
}
// Обработчик для callback
async fn github_callback(
State(oauth_client): State<Arc<OAuthClient>>,
Query(params): Query<AuthRequest>,
) -> impl IntoResponse {
// Обмен кода на токен
let token = oauth_client
.client
.exchange_code(AuthorizationCode::new(params.code))
.request_async(async_http_client)
.await;
match token {
Ok(token) => {
// Получение данных пользователя с помощью токена
let client = reqwest::Client::new();
let user_data = client
.get("https://api.github.com/user")
.header("Authorization", format!("Bearer {}", token.access_token().secret()))
.header("User-Agent", "Axum OAuth Example")
.send()
.await;
match user_data {
Ok(response) => {
if let Ok(user_json) = response.json::<serde_json::Value>().await {
// В реальном приложении здесь создание сессии или JWT
format!("Успешная аутентификация: {}", user_json)
} else {
"Ошибка при получении данных пользователя".to_string()
}
},
Err(_) => "Ошибка при запросе данных пользователя".to_string(),
}
},
Err(_) => "Ошибка при обмене кода на токен".to_string(),
}
}
// Настройка маршрутов OAuth
fn oauth_routes() -> Router {
let oauth_client = Arc::new(create_github_client());
Router::new()
.route("/auth/github", get(github_auth))
.route("/auth/github/callback", get(github_callback))
.with_state(oauth_client)
}
Базовая HTTP-аутентификация
Для простых случаев можно использовать базовую HTTP-аутентификацию:
use axum::{
async_trait,
extract::{FromRequest, RequestParts, TypedHeader},
headers::authorization::{Authorization, Basic},
http::StatusCode,
response::IntoResponse,
routing::get,
Router,
};
// Экстрактор для базовой аутентификации
#[async_trait]
impl<B> FromRequest<B> for AuthUser
where
B: Send,
{
type Rejection = StatusCode;
async fn from_request(req: &mut RequestParts<B>) -> Result<Self, Self::Rejection> {
// Извлечение заголовка Basic Authentication
let TypedHeader(Authorization(basic)) =
TypedHeader::<Authorization<Basic>>::from_request(req)
.await
.map_err(|_| StatusCode::UNAUTHORIZED)?;
// Проверка учетных данных
if basic.username() == "admin" && basic.password() == "password" {
Ok(AuthUser {
user_id: "1".to_string(),
name: basic.username().to_string(),
role: "admin".to_string(),
})
} else {
Err(StatusCode::UNAUTHORIZED)
}
}
}
// Защищенный маршрут
async fn basic_auth_route(auth_user: AuthUser) -> impl IntoResponse {
format!("Аутентификация выполнена: {}", auth_user.name)
}
// Настройка маршрутов с базовой аутентификацией
fn basic_auth_routes() -> Router {
Router::new()
.route("/basic-auth", get(basic_auth_route))
}
Интеграция с Middleware
Для глобальной защиты маршрутов можно использовать middleware:
use axum::{
middleware::{self, Next},
response::Response,
Router,
};
use tower::ServiceBuilder;
// Middleware для проверки аутентификации
async fn auth_middleware(
auth_result: Result<AuthUser, StatusCode>,
request: Request,
next: Next,
) -> Result<Response, StatusCode> {
// Проверка результата аутентификации
let auth_user = auth_result?;
// Добавление данных пользователя в request extensions
request.extensions_mut().insert(auth_user);
// Передача управления следующему слою
Ok(next.run(request).await)
}
// Настройка приложения с middleware
fn app_with_auth() -> Router {
let protected_routes = Router::new()
.route("/profile", get(profile))
.route("/admin", get(admin_panel))
.layer(middleware::from_fn(auth_middleware));
Router::new()
.route("/login", post(login))
.nest("/api", protected_routes)
}
// Получение пользователя из extensions
async fn profile(Extension(auth_user): Extension<AuthUser>) -> impl IntoResponse {
format!("Профиль пользователя: {}", auth_user.name)
}
// Проверка роли
async fn admin_panel(Extension(auth_user): Extension<AuthUser>) -> Result<String, StatusCode> {
if auth_user.role == "admin" {
Ok("Панель администратора".to_string())
} else {
Err(StatusCode::FORBIDDEN)
}
}
Лучшие практики
Безопасное хранение паролей
rustuse argon2::{ password_hash::{rand_core::OsRng, PasswordHash, PasswordHasher, PasswordVerifier, SaltString}, Argon2, }; fn hash_password(password: &str) -> Result<String, String> { let salt = SaltString::generate(&mut OsRng); let argon2 = Argon2::default(); argon2 .hash_password(password.as_bytes(), &salt) .map(|hash| hash.to_string()) .map_err(|e| format!("Ошибка хеширования: {}", e)) } fn verify_password(hash: &str, password: &str) -> Result<bool, String> { let parsed_hash = PasswordHash::new(hash) .map_err(|e| format!("Ошибка при разборе хеша: {}", e))?; Ok(Argon2::default() .verify_password(password.as_bytes(), &parsed_hash) .is_ok()) }
Защита от CSRF-атак
rust// Генерация CSRF-токена fn generate_csrf_token() -> String { use rand::{thread_rng, Rng}; use rand::distributions::Alphanumeric; thread_rng() .sample_iter(&Alphanumeric) .take(32) .map(char::from) .collect() } // Проверка CSRF-токена в middleware async fn csrf_middleware(mut request: Request, next: Next) -> Result<Response, StatusCode> { // Только для POST/PUT/DELETE запросов if matches!(request.method(), &Method::POST | &Method::PUT | &Method::DELETE) { let cookie_token = request .headers() .get("Cookie") .and_then(|c| { c.to_str().ok()?.split(';') .find_map(|part| { let part = part.trim(); if part.starts_with("csrf=") { Some(part[5..].to_string()) } else { None } }) }) .ok_or(StatusCode::FORBIDDEN)?; let header_token = request .headers() .get("X-CSRF-Token") .and_then(|h| h.to_str().ok()) .ok_or(StatusCode::FORBIDDEN)?; if cookie_token != header_token { return Err(StatusCode::FORBIDDEN); } } Ok(next.run(request).await) }
Управление сессиями
- Устанавливайте флаг
HttpOnly
для предотвращения доступа к cookie через JavaScript - Используйте флаг
Secure
для передачи cookie только через HTTPS - Устанавливайте
SameSite=Lax
илиSameSite=Strict
для защиты от CSRF - Регулярно обновляйте ID сессии для предотвращения атак фиксации сессии
- Устанавливайте флаг
Защита от атак перебора
rustuse std::collections::HashMap; use std::sync::{Arc, Mutex}; use std::time::{Duration, Instant}; // Структура для отслеживания попыток входа struct LoginAttemptTracker { attempts: HashMap<String, Vec<Instant>>, max_attempts: usize, window_seconds: u64, } impl LoginAttemptTracker { fn new(max_attempts: usize, window_seconds: u64) -> Self { Self { attempts: HashMap::new(), max_attempts, window_seconds, } } fn is_rate_limited(&mut self, ip: &str) -> bool { let now = Instant::now(); let window = Duration::from_secs(self.window_seconds); // Очистка устаревших попыток if let Some(attempts) = self.attempts.get_mut(ip) { attempts.retain(|time| now.duration_since(*time) < window); // Проверка количества попыток if attempts.len() >= self.max_attempts { return true; } // Добавление новой попытки attempts.push(now); } else { self.attempts.insert(ip.to_string(), vec![now]); } false } } // Использование в обработчике логина async fn login_with_rate_limit( State(tracker): State<Arc<Mutex<LoginAttemptTracker>>>, ConnectInfo(addr): ConnectInfo<SocketAddr>, Json(payload): Json<LoginRequest>, ) -> Result<impl IntoResponse, StatusCode> { let ip = addr.ip().to_string(); // Проверка ограничения скорости if tracker.lock().unwrap().is_rate_limited(&ip) { return Err(StatusCode::TOO_MANY_REQUESTS); } // Обычная логика входа if authenticate(&payload.username, &payload.password).await { Ok("Успешный вход") } else { Err(StatusCode::UNAUTHORIZED) } }
Ограничение срока действия токенов
- Устанавливайте короткий срок действия для access токенов (минуты или часы)
- Используйте refresh токены для продления сессии
- Храните список отозванных токенов для немедленного прекращения доступа
Аутентификация в Axum может быть реализована различными способами в зависимости от требований приложения. Выбор между JWT, сессиями или OAuth зависит от характера приложения, требований к безопасности и удобству пользователей.