如何在actix-web中创建受保护的路由?

34

我需要验证用户是否有权限访问某些路由。

我已经创建了三个“作用域”(guest,auth-user,admin),现在我不知道如何检查用户是否可以访问这些路由。

我正在尝试实现身份验证中间件,该中间件应该检查用户是否具有正确的cookie或令牌。(我能够从请求头中打印出cookie),但我不知道如何导入、使用actix_identity,并在此中间件内部访问id参数。

我相信我的问题不仅涉及Actix-identity,而且我无法在中间件内部传递参数。

    #[actix_rt::main]
    async fn main() -> std::io::Result<()> {

        let cookie_key = conf.server.key;
    
        // Register http routes
        let mut server = HttpServer::new(move || {
            App::new()
                // Enable logger
                .wrap(Logger::default())
                .wrap(IdentityService::new(
                    CookieIdentityPolicy::new(cookie_key.as_bytes())
                        .name("auth-cookie")
                        .path("/")
                        .secure(false),
                ))
                //limit the maximum amount of data that server will accept
                .data(web::JsonConfig::default().limit(4096))
                //normal routes
                .service(web::resource("/").route(web::get().to(status)))
                // .configure(routes)
                .service(
                    web::scope("/api")
                        // guest endpoints
                        .service(web::resource("/user_login").route(web::post().to(login)))
                        .service(web::resource("/user_logout").route(web::post().to(logout)))
                        // admin endpoints
                        .service(
                            web::scope("/admin")
                                // .wrap(AdminAuthMiddleware)
                                .service(
                                    web::resource("/create_admin").route(web::post().to(create_admin)),
                                )
                                .service(
                                    web::resource("/delete_admin/{username}/{_:/?}")
                                        .route(web::delete().to(delete_admin)),
                                ),
                        )
                        //user auth routes
                        .service(
                            web::scope("/auth")
                                // .wrap(UserAuthMiddleware)
                                .service(web::resource("/get_user").route(web::get().to(get_user))),
                        ),
                )
        });
    
        // Enables us to hot reload the server
        let mut listenfd = ListenFd::from_env();
        server = if let Some(l) = listenfd.take_tcp_listener(0).unwrap() {
            server.listen(l)?
        } else {
            server.bind(ip)?
        };
    
        server.run().await

我尝试过的资源:

  1. 为Actix API创建身份验证中间件 https://www.jamesbaum.co.uk/blether/creating-authentication-middleware-actix-rust-react/

  2. 在middleware中进行Actix-web令牌验证 https://users.rust-lang.org/t/actix-web-token-validation-in-middleware/38205

  3. Actix middleware示例 https://github.com/actix/examples/tree/master/middleware

也许我完全想错了,auth-middleware不是解决我的问题的最佳方案。 我希望您可以帮助我创建“受保护的路由”。

5个回答

32

尝试使用提取器

在尝试将这个模式实现到Actix 3中时,我曾经头疼了一段时间,试图使用中间件来创建一个守卫并找出如何将数据从中间件传递到处理程序。这让我很痛苦,最终我意识到我是在与Actix对抗而不是与之合作。

最后我学会了通过创建一个结构体(也许是AuthedUser),并在该结构体上实现FromRequest特征来将信息传递给处理程序。

然后,每个在函数签名中请求AuthedUser的处理程序都将进行身份验证,并且如果用户已登录,则FromRequest::from_request方法中附加到AuthedUser的任何用户信息都将随之传递。

Actix将实现FromRequest特征的这些结构体称为提取器。这是一种有点神奇的技巧,在指南中需要更多关注。


1
起初,我错误地解释了这个答案,这让我认为编写(自定义)中间件以使信息可用于处理程序是复杂或不可能的。如果其他人也有同样的想法,那么可以编写一个中间件来“保护”您的路由并在处理程序中公开信息(会话/用户)。在您的中间件中: req.extensions_mut().insert(session);在您的处理程序中使用ReqData来提取它 session: web::ReqData<Session>Actix Identity -> Identity结构实现了FromRequest特性,Identity是一个提取器。 - Brecht De Rooms

11
以下内容不使用中间件(需要多做一些工作),但它解决了问题,并似乎是文档建议的方法:
#[macro_use]
extern crate actix_web;
use actix::prelude::*;
use actix_identity::{CookieIdentityPolicy, Identity, IdentityService};
use actix_web::{
    dev::Payload, error::ErrorUnauthorized, web, App, Error, FromRequest, HttpRequest,
    HttpResponse, HttpServer, Responder,
};
use log::{info, warn};
use serde::{Deserialize, Serialize};
use std::{collections::HashMap, pin::Pin, sync::RwLock};

#[derive(Serialize, Deserialize, Debug, Default, Clone)]
struct Sessions {
    map: HashMap<String, User>,
}

#[derive(Serialize, Deserialize, Debug, Default, Clone)]
#[serde(rename_all = "camelCase")]
struct Login {
    id: String,
    username: String,
    scope: Scope,
}

#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
#[serde(rename_all = "camelCase")]
enum Scope {
    Guest,
    User,
    Admin,
}

impl Default for Scope {
    fn default() -> Self {
        Scope::Guest
    }
}

#[derive(Serialize, Deserialize, Debug, Default, Clone)]
#[serde(rename_all = "camelCase")]
struct User {
    id: String,
    first_name: Option<String>,
    last_name: Option<String>,
    authorities: Scope,
}

impl FromRequest for User {
    type Config = ();
    type Error = Error;
    type Future = Pin<Box<dyn Future<Output = Result<User, Error>>>>;

    fn from_request(req: &HttpRequest, pl: &mut Payload) -> Self::Future {
        let fut = Identity::from_request(req, pl);
        let sessions: Option<&web::Data<RwLock<Sessions>>> = req.app_data();
        if sessions.is_none() {
            warn!("sessions is empty(none)!");
            return Box::pin(async { Err(ErrorUnauthorized("unauthorized")) });
        }
        let sessions = sessions.unwrap().clone();
        Box::pin(async move {
            if let Some(identity) = fut.await?.identity() {
                if let Some(user) = sessions
                    .read()
                    .unwrap()
                    .map
                    .get(&identity)
                    .map(|x| x.clone())
                {
                    return Ok(user);
                }
            };

            Err(ErrorUnauthorized("unauthorized"))
        })
    }
}

#[get("/admin")]
async fn admin(user: User) -> impl Responder {
    if user.authorities != Scope::Admin {
        return HttpResponse::Unauthorized().finish();
    }
    HttpResponse::Ok().body("You are an admin")
}

#[get("/account")]
async fn account(user: User) -> impl Responder {
    web::Json(user)
}

#[post("/login")]
async fn login(
    login: web::Json<Login>,
    sessions: web::Data<RwLock<Sessions>>,
    identity: Identity,
) -> impl Responder {
    let id = login.id.to_string();
    let scope = &login.scope;
    //let user = fetch_user(login).await // from db?
    identity.remember(id.clone());
    let user = User {
        id: id.clone(),
        last_name: Some(String::from("Doe")),
        first_name: Some(String::from("John")),
        authorities: scope.clone(),
    };
    sessions.write().unwrap().map.insert(id, user.clone());
    info!("login user: {:?}", user);
    HttpResponse::Ok().json(user)
}

#[post("/logout")]
async fn logout(sessions: web::Data<RwLock<Sessions>>, identity: Identity) -> impl Responder {
    if let Some(id) = identity.identity() {
        identity.forget();
        if let Some(user) = sessions.write().unwrap().map.remove(&id) {
            warn!("logout user: {:?}", user);
        }
    }
    HttpResponse::Unauthorized().finish()
}

#[actix_rt::main]
async fn main() -> std::io::Result<()> {
    env_logger::init();

    let sessions = web::Data::new(RwLock::new(Sessions {
        map: HashMap::new(),
    }));

    HttpServer::new(move || {
        App::new()
            .app_data(sessions.clone())
            .wrap(IdentityService::new(
                CookieIdentityPolicy::new(&[0; 32])
                    .name("test")
                    .secure(false),
            ))
            .service(account)
            .service(login)
            .service(logout)
            .service(admin)
    })
    .bind("127.0.0.1:8088")?
    .run()
    .await
}


你可以在这里克隆并运行它:https://github.com/geofmureithi/actix-acl-example

6
我认为actix-web grants crate非常适合您。它允许您使用Guard或过程宏(请参见Github上的示例)进行授权检查。它还与现有的授权中间件(如actix-web-httpauth)很好地集成。

以下是一些示例,以便更清楚:

  • 通过proc-macro方式
#[get("/secure")]
#[has_permissions("ROLE_ADMIN")]
async fn macro_secured() -> HttpResponse {
    HttpResponse::Ok().body("ADMIN_RESPONSE")
}
  • 守卫之路
App::new()
    .wrap(GrantsMiddleware::with_extractor(extract))
    .service(web::resource("/admin")
            .to(|| async { HttpResponse::Ok().finish() })
            .guard(PermissionGuard::new("ROLE_ADMIN".to_string())))

你还可以查看actix-casbin-auth(将casbin集成到actix中的实现)


2

中间件定义了许多泛型和内部类型,看起来并不友好,但它只是一个简单的结构体,用于包装下一个要调用的服务。下一个服务是由创建应用程序或定义路由时的链式调用确定的。在中间件中使用泛型S,它将在编译时进行单态化,因此您无需关心中间件将保护哪种具体类型。

以下中间件使用通过.data()传递给您的应用程序的简单配置来检查“token”标头是否包含相同的魔术值。它可以通过下一个服务或返回未授权错误(futures)。

use crate::config::Config;

use actix_service::{Service, Transform};
use actix_web::{
    dev::{ServiceRequest, ServiceResponse},
    error::ErrorUnauthorized,
    web::Data,
    Error,
};
use futures::future::{err, ok, Either, Ready};
use std::task::{Context, Poll};

pub struct TokenAuth;

impl<S, B> Transform<S> for TokenAuth
where
    S: Service<Request = ServiceRequest, Response = ServiceResponse<B>, Error = Error>,
    S::Future: 'static,
    B: 'static,
{    type Request = ServiceRequest;
    type Response = ServiceResponse<B>;
    type Error = Error;
    type InitError = ();
    type Transform = TokenAuthMiddleware<S>;
    type Future = Ready<Result<Self::Transform, Self::InitError>>;

    fn new_transform(&self, service: S) -> Self::Future {
        ok(TokenAuthMiddleware { service })
    }
}

pub struct TokenAuthMiddleware<S> {
    service: S,
}

impl<S, B> Service for TokenAuthMiddleware<S>
where
    S: Service<Request = ServiceRequest, Response = ServiceResponse<B>, Error = Error>,
    S::Future: 'static,
{    type Request = ServiceRequest;
    type Response = ServiceResponse<B>;
    type Error = Error;
    type Future = Either<S::Future, Ready<Result<Self::Response, Self::Error>>>;

    fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
        self.service.poll_ready(cx)
    }

    fn call(&mut self, req: ServiceRequest) -> Self::Future {
        if let Some(token) = req
            .headers()
            .get("token")
            .and_then(|token| token.to_str().ok())
        {
            if let Some(config) = req.app_data::<Data<Config>>() {
                if token == config.token {
                    return Either::Left(self.service.call(req));
                }
            }
        }
        Either::Right(err(ErrorUnauthorized("not authorized")))
    }
}

保护你的函数就像简单地这样。
#[post("/upload", wrap="TokenAuth")]
async fn upload(mut payload: Multipart) -> Result<HttpResponse, Error> {
}

请注意,为了编译此内容,您需要使用actix_service 1.x。actix_service 2删除了请求内部类型以使其通用化,我无法使用wrap =“”语法使其工作。

1

在最新的actix-web 3.0版本中,实现这个功能相当困难。我的做法是从actix-web 1.0版本中复制CookieIdentityPolicy中间件,并对其进行修改以适应我的需求。然而,这不是即插即用的代码。这里这里是我的版本。一般来说,我会避免使用actix-web,在后台生成线程/actor并执行HTTP请求是一场噩梦。尝试与处理程序共享结果更加困难。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接