[Rust开发]actix_webmiddleware 中间件

actix_web::middleware 在 Actix Web 框架中扮演着重要的角色,它允许开发者在处理 HTTP 请求和响应的过程中插入自定义的逻辑。中间件可以在请求到达处理函数之前或响应返回给客户端之前执行,从而实现日志记录、身份验证、数据验证、错误处理等功能。

为什么要有中间件?

代码在处理逻辑的时候,通常只处理正常的业务逻辑,而在处理过程中,可能会遇到一些特殊的情况,比如:404错误,这种错误会让客户端的请求无法进入到代码功能中,这时候就需要中间件来处理这些特殊的情况。

另外,还可以起到Java里面的过滤器功能,在Java里面,过滤器可以在请求到达处理函数之前或响应返回给客户端之前执行,从而实现对请求和响应的预处理和后处理。

在Rust开发中,特别是在使用Actix Web框架时,中间件(middleware)是一种非常有用的设计模式,它允许开发者在处理HTTP请求和响应的过程中插入自定义的逻辑。以下是使用中间件的几个主要原因:

  • 代码模块化和可重用性:中间件可以将通用的功能(如日志记录、身份验证、数据验证等)封装起来,使得这些功能可以被多个处理函数共享和重用。这样可以减少代码重复,提高代码的可读性和可维护性。

  • 灵活性和可扩展性:通过使用中间件,开发者可以灵活地组合和配置不同的功能,以满足应用程序的特定需求。例如,可以根据不同的路由或用户角色应用不同的中间件。

  • 请求和响应的预处理和后处理:中间件可以在请求到达处理函数之前或响应返回给客户端之前执行,从而实现对请求和响应的预处理和后处理。例如,可以在请求到达之前记录日志、验证用户身份,或者在响应返回之前压缩数据、添加额外的头部信息等。

  • 提高开发效率:使用中间件可以减少开发者编写重复代码的工作量,使得开发者可以更专注于业务逻辑的实现。同时,中间件的模块化设计也使得代码的测试和调试更加容易。

  • 支持异步编程模型:Actix Web是一个基于异步IO的Web框架,中间件可以很好地适应这种编程模型,允许开发者在不阻塞主线程的情况下执行耗时的操作。

以下是一些常见的中间件及其作用:

  • 日志记录:记录每个请求的详细信息,如请求方法、路径、时间戳等,有助于调试和监控。
  • 身份验证:验证用户的身份,确保只有经过授权的用户才能访问特定的资源或执行特定的操作。
  • 数据验证:在请求到达处理函数之前,验证请求数据的格式和内容是否符合预期,防止无效或恶意数据进入系统。
  • 错误处理:统一处理应用程序中的错误,提供友好的错误信息给客户端,同时记录错误日志以便后续分析。
  • 性能监控:测量每个请求的处理时间,帮助开发者识别性能瓶颈并进行优化。
  • 跨域资源共享(CORS):处理跨域请求,允许或拒绝来自不同域的请求,确保安全的跨域数据交互。
  • 使用中间件可以使代码更加模块化和可重用,开发者可以根据需要组合和配置不同的中间件,以满足应用程序的特定需求。在 Actix Web 中,中间件通常通过 App::wrap 方法进行注册和应用。

自定义中间件的过程:

利用闭包实现简单的中间件

例如我要定义一个预先获取请求中header里面的token,如果没有这个token则直接就返回错误。代码可以这样写:

复制代码
use actix_web::dev::Service;
use actix_web::error;
use actix_web::{middleware::{self, Logger}, web, App, HttpServer};
#[actix_web::main]
async fn main() -> std::io::Result<()> {
    HttpServer::new(move || {
        App::new()
            .wrap(middleware::Logger::default())
            .wrap_fn(|req, srv| {
                let header = req.headers().to_owned();
                let fut = srv.call(req);
                async move {
                    let res = fut.await;
                    match header.get("token"){
                        Some(token) => {
                            println!("token:{}", token.to_str().unwrap());
                        },
                        None => {
                            println!("token is None");
                            return Err(error::ErrorUnauthorized("Unauthorized"));
                        },
                    }
                    res
                }
            })
            .route("/", web::get().to(|| async { "Hello, World!" }))
    })
    .bind("127.0.0.1:8080")?
    .run()
    .await
}

这里的核心,是使用wrap_fn + 一个闭包来实现中间件。

在示例代码中,wrap_fn 被用来创建一个中间件,这个中间件检查请求头中是否包含 token。如果没有 token,它会返回一个未经授权的错误。如果有 token,它会调用下一个服务并返回其结果。

warp_fn的源码如下(看不懂也没关系,不用懂,我写这里是为了你以后能看懂时候回来查资料用的)

复制代码
pub fn wrap_fn<F, Fut>(f: F) -> impl Transform<ServiceRequest, Response = ServiceResponse<Fut::Item>, Error = Fut::Error, InitError = ()>
where
    F: Fn(ServiceRequest, &mut dyn Service<ServiceRequest, Response = ServiceResponse<Fut::Item>, Error = Fut::Error, Future = Fut>) -> Fut,
    Fut: Future<Output = Result<ServiceResponse<Fut::Item>, Fut::Error>> + 'static,

里面参数解释如下:

  • 输入参数:

    • F: 这是一个函数类型,它接受两个参数:ServiceRequest 和一个可变引用 &mut dyn Service<...>。这个函数将返回一个 Fut 类型的 Future。
    • Fut: 这是一个 Future 类型,它的输出是 Result<ServiceResponseFut::Item, Fut::Error>。Fut::Item 是响应体的类型,Fut::Error 是错误类型。
  • 返回值:

    • impl Transform<ServiceRequest, Response = ServiceResponseFut::Item, Error = Fut::Error, InitError = ()>: 这是一个实现了 Transform trait 的类型,它可以转换 ServiceRequest 到 ServiceResponseFut::Item,并且可以处理 Fut::Error 类型的错误。

执行结果如下:

如果是一些建议的逻辑,我们用wrap_fn + 闭包就可以了,但是如果是一些复杂的逻辑,就需要自己实现Transform trait了。

重新实现Transform trait 来实现自定义的中间件
  1. 中间件初始化:在这个阶段,中间件工厂函数被调用,它接收链中的下一个服务作为参数。这允许中间件在实际处理请求之前进行任何必要的设置或配置。
  • 中间件工厂类是Transform``trait的。

    • S - 后续服务的类型:S 代表链中下一个服务的类型,即当前中间件将请求传递给哪个服务。
    • B - 响应体的类型:B 代表响应体(response)的类型,指定了从服务返回的响应内容的格式。

    //中间的工厂类

    pub struct Auth;

    impl<S, B> Transform<S, ServiceRequest> for Auth

    where

    S: Service<ServiceRequest, Response = ServiceResponse**, Error = Error>,
    S::Future: 'static,
    B: 'static,
    {
    type Response = ServiceResponse**;
    type Error = Error;
    type InitError = ();
    type Transform = AuthMiddleware;
    type Future = Ready<Result<Self::Transform, Self::InitError>>;****

    复制代码
    fn new_transform(&self, service: S) -> Self::Future {
        ready(Ok(AuthMiddleware { service }))
    }

    }

  1. 编写中间件的具体实现,核心是中间件的 call 方法调用:一旦中间件初始化完成,每当有新的请求到来时,中间件的 call 方法就会被调用,并接收这个普通请求作为参数。此时,中间件可以对请求进行处理

    // 中间件的具体实现,里面需要接受工厂类里面过来的service

    pub struct AuthMiddleware {
    service: S,
    }

    //具体实现

    //核心是两个方法:

    // call 具体实现

    // poll_ready

    impl<S, B> Service for AuthMiddleware
    where
    S: Service<ServiceRequest, Response = ServiceResponse**, Error = Error>,
    S::Future: 'static,
    B: 'static,
    {
    type Response = ServiceResponse**;
    type Error = Error;
    type Future = LocalBoxFuture<'static, Result<Self::Response, Self::Error>>;****

    复制代码
    // 实现 poll_ready 方法,用于检查服务是否准备好处理请求 
    //这里用的是forward_ready!宏
    forward_ready!(service);
    
    // 实现 call 方法,用于处理实际的请求
    fn call(&self, req: ServiceRequest) -> Self::Future {
        // 进行鉴权操作,判断是否有权限
        if has_permission(&req) {
            // 有权限,继续执行后续中间件
            let fut = self.service.call(req);
            Box::pin(async move {
                let res = fut.await?;
                Ok(res)
            })
        } else {
            // 没有权限,立即返回响应
            Box::pin(async move {
                // 鉴权失败,返回未授权的响应,停止后续中间件的调用
                Err(error::ErrorUnauthorized("Unauthorized"))
            })
        }
    }

    }

    fn has_permission(req: &ServiceRequest) -> bool {

    // 实现你的鉴权逻辑,根据需求判断是否有权限

    // 返回 true 表示有权限,返回 false 表示没有权限

    // unimplemented!()

    let value = HeaderValue::from_str("").unwrap();

    match req.path().to_ascii_lowercase().as_str(){

    "/login" => true,

    _ => {

    let token = req.headers().get("token").unwrap_or(&value);

    if token.len() <=0{

    false

    }else{

    println!("验证一下token,看看是否合法");

    true

    }

    }

    }

    }

核心方法说明:
  • poll_ready: 这个方法用于检查服务是否准备好处理请求。 它返回一个Poll类型的结果,表示服务是否就绪。 如果服务已经就绪,返回Poll::Ready(Ok(()))。 如果服务尚未就绪,返回Poll::Pending,表示需要等待一段时间后再次检查。 在middleware中,poll_ready方法通常用于确保在处理请求之前,所有依赖的资源或服务都已经准备就绪。

  • call: 这个方法用于处理实际的请求。 它接收一个ServiceRequest类型的参数,并返回一个ServiceResponse类型的结果。 在middleware中,call方法通常用于对请求进行预处理或后处理,例如添加日志记录、验证请求、修改响应等。 call方法的返回值是一个Future,表示异步处理的结果。

所以也可以自己实现poll_ready

复制代码
fn poll_ready(&self, ctx: &mut core::task::Context<'_>) -> std::task::Poll<Result<(), Self::Error>> {
        if self.service.poll_ready(ctx).is_pending() {
            // 如果服务尚未准备好,返回Pending
            return std::task::Poll::Pending;
        }

        // 如果服务已准备好,返回Ready(Ok(()))
        std::task::Poll::Ready(Ok(()))
}

使用中间件

复制代码
#[actix_web::main]
async fn main() -> std::io::Result<()> {
    //中间件的顺序是从下到上的,最后注册的中间件会最先执行
    HttpServer::new(move || {
        App::new()
            .wrap(middleware::Logger::default())
            .wrap(Auth::Auth)
            // 注册其他路由和处理函数
            .route("/", web::get().to(|| async { "Hello, World!" }))
            .route("/login", web::get().to(|| async { "Hello, login" }))
    })
    .bind("127.0.0.1:8080")?
    .run()
    .await
}

执行结果如下:

相关推荐
Scott9999HH8 小时前
【IIoT流量实战】蒸汽管道阀门全关却仍有流量?用 Python 实现涡街信号 FFT 频谱分析与温压全补偿积算网关,深度拆解靠谱的涡街流量计厂家硬核技术标准
开发语言·python
码智社9 小时前
AES加密原理详解及Java实现加解密实战
java·开发语言
AI云海9 小时前
python 列表、元组、集合和字典
开发语言·python
萧瑟余晖10 小时前
JDK 26 新特性详解
java·开发语言
马优晨10 小时前
Freemarker 完整讲解(后端 Java 模板引擎)
java·开发语言·freemarker·freemarker 完整讲解·freemarker模板引擎
doiito12 小时前
【AI 应用】从“外国人味”到地道中文:kokoroi-rs v0.1.2 架构升级深度解析
ai·rust·系统设计
人邮异步社区12 小时前
怎么把C语言学到精通?
c语言·开发语言
心平气和量大福大13 小时前
C#-WPF-控件-TextBox 数据绑定
开发语言·c#·wpf
ttwuai13 小时前
Cursor 生成 CRUD 后,Go 后台接口别只测 200:JWT、RBAC 和 tenant_id 怎么验
开发语言·后端·golang
এ慕ོ冬℘゜13 小时前
前端基础:什么是时间戳?JS获取时间戳三种方法与实战用途
开发语言·前端·javascript