Почему Rust — идеальный выбор для аутентификации?
Если вы пишете веб-приложение на Rust, вы уже на шаг впереди по безопасности. Rust не прощает ошибок с памятью, а значит, многие уязвимости (переполнение буфера, use-after-free) отсекаются на этапе компиляции. Но компилятор не защитит вас от плохой логики аутентификации. Именно об этом — как построить безопасную аутентификацию в Rust с использованием современных практик.
В этой статье мы соберём полноценный модуль аутентификации: хеширование паролей через bcrypt, выпуск JWT-токенов, middleware-проверку в Actix-web и защиту от типовых атак. Код будет рабочим — можете смело брать его в свой проект.
1. Хеширование паролей: почему bcrypt, а не SHA-256?
Многие новички думают: «Возьму SHA-256, он быстрый и надёжный». И это фатальная ошибка. SHA — криптографическая хеш-функция, но она слишком быстрая. Атакующий может перебирать миллиарды комбинаций в секунду. Нам нужна медленная функция, специально спроектированная для паролей.
bcrypt — золотой стандарт. Он включает «соль» (salt) и «фактор стоимости» (cost factor), который можно увеличивать по мере роста вычислительных мощностей.
Добавляем bcrypt в Cargo.toml
[dependencies]
bcrypt = "0.15"
Хешируем пароль
use bcrypt::{hash, verify, DEFAULT_COST};
fn hash_password(plain: &str) -> Result<String, bcrypt::BcryptError> {
hash(plain, DEFAULT_COST) // cost = 12 (по умолчанию)
}
fn verify_password(plain: &str, hashed: &str) -> bool {
verify(plain, hashed).unwrap_or(false)
}
Обратите внимание: verify сам достаёт соль из хеша. Вам не нужно хранить соль отдельно — она уже внутри строки хеша. Это безопасно и удобно.
2. JWT-токены: как не провалить аутентификацию
После того как пользователь ввёл пароль, мы должны выдать ему «пропуск». JWT (JSON Web Token) — компактный и самодостаточный формат. Но дьявол в деталях:
- Не храните секретный ключ в коде. Используйте переменные окружения.
- Устанавливайте короткий срок жизни. Access-токен — 15–30 минут, refresh-токен — 7–30 дней.
- Не вставляйте чувствительные данные в payload. Только ID пользователя и роль.
Реализация JWT с jsonwebtoken
use jsonwebtoken::{encode, decode, Header, Validation, EncodingKey, DecodingKey};
use serde::{Serialize, Deserialize};
use chrono::{Utc, Duration};
#[derive(Debug, Serialize, Deserialize)]
pub struct Claims {
pub sub: String, // ID пользователя
pub exp: usize, // expiration timestamp
pub iat: usize, // issued at
}
pub fn create_token(user_id: &str, secret: &str) -> Result<String, jsonwebtoken::errors::Error> {
let now = Utc::now();
let claims = Claims {
sub: user_id.to_owned(),
exp: (now + Duration::minutes(30)).timestamp() as usize,
iat: now.timestamp() as usize,
};
encode(&Header::default(), &claims, &EncodingKey::from_secret(secret.as_bytes()))
}
pub fn validate_token(token: &str, secret: &str) -> Result<Claims, jsonwebtoken::errors::Error> {
let token_data = decode::<Claims>(
token,
&DecodingKey::from_secret(secret.as_bytes()),
&Validation::default(),
)?;
Ok(token_data.claims)
}
3. Собираем всё вместе: Actix-web + middleware
Теперь соединим хеширование и JWT в полноценном веб-приложении на Actix-web. Напишем middleware, который будет проверять токен в каждом защищённом запросе.
Структура проекта
src/
├── main.rs
├── auth.rs # хеширование, JWT
├── middleware.rs # проверка токена
└── handlers.rs # регистрация, логин, профиль
auth.rs — полный модуль
use bcrypt::{hash, verify, DEFAULT_COST};
use jsonwebtoken::{encode, decode, Header, Validation, EncodingKey, DecodingKey};
use serde::{Deserialize, Serialize};
use chrono::{Utc, Duration};
#[derive(Debug, Serialize, Deserialize)]
pub struct Claims {
pub sub: String,
pub exp: usize,
pub iat: usize,
}
pub fn hash_pwd(plain: &str) -> Result<String, String> {
hash(plain, DEFAULT_COST).map_err(|e| e.to_string())
}
pub fn verify_pwd(plain: &str, hashed: &str) -> bool {
verify(plain, hashed).unwrap_or(false)
}
pub fn gen_token(user_id: &str, secret: &str) -> Result<String, String> {
let now = Utc::now();
let claims = Claims {
sub: user_id.to_owned(),
exp: (now + Duration::minutes(30)).timestamp() as usize,
iat: now.timestamp() as usize,
};
encode(&Header::default(), &claims, &EncodingKey::from_secret(secret.as_bytes()))
.map_err(|e| e.to_string())
}
pub fn validate_token(token: &str, secret: &str) -> Result<Claims, String> {
decode::<Claims>(
token,
&DecodingKey::from_secret(secret.as_bytes()),
&Validation::default(),
)
.map(|data| data.claims)
.map_err(|e| e.to_string())
}
middleware.rs — проверка токена
use actix_web::{dev::{ServiceRequest, ServiceResponse, Transform, Service}, Error, HttpMessage};
use futures_util::future::LocalBoxFuture;
use std::rc::Rc;
use crate::auth;
pub struct AuthMiddleware {
pub secret: Rc<String>,
}
impl<S, B> Transform<S, ServiceRequest> for AuthMiddleware
where
S: Service<ServiceRequest, Response = ServiceResponse<B>, Error = Error> + 'static,
S::Future: 'static,
B: 'static,
{
type Response = ServiceResponse<B>;
type Error = Error;
type Transform = AuthMiddlewareService<S>;
type InitError = ();
type Future = LocalBoxFuture<'static, Result<Self::Transform, Self::InitError>>;
fn new_transform(&self, service: S) -> Self::Future {
let secret = self.secret.clone();
Box::pin(async move {
Ok(AuthMiddlewareService {
service,
secret,
})
})
}
}
pub struct AuthMiddlewareService<S> {
service: S,
secret: Rc<String>,
}
impl<S, B> Service<ServiceRequest> for AuthMiddlewareService<S>
where
S: Service<ServiceRequest, Response = ServiceResponse<B>, Error = Error>,
S::