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 来实现自定义的中间件
- 中间件初始化:在这个阶段,中间件工厂函数被调用,它接收链中的下一个服务作为参数。这允许中间件在实际处理请求之前进行任何必要的设置或配置。
- 中间件工厂类是
Transform
trait
的。- S - 后续服务的类型:S 代表链中下一个服务的类型,即当前中间件将请求传递给哪个服务。
- B - 响应体的类型:B 代表响应体(response)的类型,指定了从服务返回的响应内容的格式。
//中间的工厂类
pub struct Auth;
impl<S, B> Transform<S, ServiceRequest> for Auth
where
S: Service<ServiceRequest, Response = ServiceResponse<B>, Error = Error>,
S::Future: 'static,
B: 'static,
{
type Response = ServiceResponse<B>;
type Error = Error;
type InitError = ();
type Transform = AuthMiddleware<S>;
type Future = Ready<Result<Self::Transform, Self::InitError>>;
fn new_transform(&self, service: S) -> Self::Future {
ready(Ok(AuthMiddleware { service }))
}
}
- 编写中间件的具体实现,核心是中间件的 call 方法调用:一旦中间件初始化完成,每当有新的请求到来时,中间件的 call 方法就会被调用,并接收这个普通请求作为参数。此时,中间件可以对请求进行处理
// 中间件的具体实现,里面需要接受工厂类里面过来的service
pub struct AuthMiddleware<S> {
service: S,
}
//具体实现
//核心是两个方法:
// call 具体实现
// poll_ready
impl<S, B> Service<ServiceRequest> for AuthMiddleware<S>
where
S: Service<ServiceRequest, Response = ServiceResponse<B>, Error = Error>,
S::Future: 'static,
B: 'static,
{
type Response = ServiceResponse<B>;
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
}
执行结果如下: