Всегда возвращайте Ok HttpResponse, затем работайте в обработчике actix-web

У меня есть обработчик для сброса пароля. Он всегда возвращает успешный код состояния 200, поэтому злоумышленник не может использовать его, чтобы узнать, какие адреса электронной почты хранятся в базе данных. Проблема в том, что если электронное письмо находится в базе данных, выполнение запроса займет некоторое время (блокировка поиска пользователя и отправка фактического электронного письма с токеном сброса). Если пользователя нет в базе данных, запрос возвращается очень быстро, поэтому атакованный будет знать, что электронной почты там нет.

Как мне сразу же вернуть ответ HTTP при обработке запроса в фоновом режиме?

pub async fn forgot_password_handler(
    email_from_path: web::Path<String>,
    pool: web::Data<Pool>,
    redis_client: web::Data<redis::Client>,
) -> HttpResponse {
    let conn: &PgConnection = &pool.get().unwrap();
    let email_address = &email_from_path.into_inner();
    // search for user with email address in users table
    match users.filter(email.eq(email_address)).first::<User>(conn) {
        Ok(user) => {
            // some stuff omitted.. this is what happens:
            // create random token for user and store a hash of it in redis (it'll expire after some time)
            // send email with password reset link and token (not hashed) to client
            // then return with 
            HttpResponse::Ok().finish(),
        }
        _ => HttpResponse::Ok().finish(),
    }   
}

person manonthemat    schedule 07.05.2020    source источник


Ответы (1)


Вы можете использовать Actix Arbiter для планирования асинхронной задачи:

use actix::Arbiter;

async fn do_the_database_stuff(
    email: String,
    pool: web::Data<Pool>,
    redis_client: web::Data<redis::Client>)
{
    // async database code here
}

pub async fn forgot_password_handler(
    email_from_path: web::Path<String>,
    pool: web::Data<Pool>,
    redis_client: web::Data<redis::Client>,
) -> HttpResponse {

    let email = email_from_path.clone();
    Arbiter::spawn(async {
        do_the_database_stuff(
            email,
            pool,
            redis_client
        );
    });

    HttpResponse::Ok().finish()
}

Если код вашей базы данных блокируется, чтобы предотвратить перегрузку долгоживущих рабочих потоков Actix, вы можете вместо этого создать новый Arbiter с его собственным потоком:

fn do_the_database_stuff(email: String) {
    // blocking database code here
}

pub async fn forgot_password_handler(email_from_path: String) -> HttpResponse {
    let email = email_from_path.clone();
    Arbiter::new().exec_fn(move || { 
        async move {               
            do_the_database_stuff(email).await; 
        };
    });

    HttpResponse::Ok().finish()
}

Это может быть немного больше работы, потому что Pool и redis::Client вряд ли будут безопасно делиться между потоками, поэтому вам также придется решить эту проблему. Вот почему я не включил их в пример кода.

Лучше использовать Arbiters, чем поддаваться соблазну создать новый собственный поток с std::thread. Если вы смешаете их, вы можете случайно включить код, который сбивает воркер с толку. Например, использование std::thread::sleep в контексте async приостановит несвязанные задачи, которые случайно были запланированы для одного и того же рабочего, и может даже не повлиять на задачу, которую вы намеревались.


Наконец, вы также можете подумать об архитектурном изменении. Если вы разложите задачи, связанные с базой данных, на их собственные микросервисы, вы решите эту проблему автоматически. Затем веб-обработчик может просто отправить сообщение (Kafka, RabbitMQ, ZMQ, HTTP или что угодно по вашему выбору) и немедленно вернуться. Это позволит вам масштабировать микросервисы независимо от веб-сервера - 10 экземпляров веб-сервера не обязательно означают 10 подключений к базе данных, если вам нужен только один экземпляр для службы сброса пароля.

person Peter Hall    schedule 07.05.2020