Безопасная аутентификация в Rust: пишем защищённый бэкенд

Онлайн-тренажер Python для начинающих

Изучайте Python легко и без перегрузки теорией. Решайте практические задачи с автоматической проверкой, получайте подсказки на русском языке и пишите код прямо в браузере — без необходимости что-либо устанавливать.

Начать курс

Почему 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::                

Блоги

Рекомендаций по книгам