文章目录
- Actix-web定义中间件(middleware)记录接口耗时
- 中间件简介
- 中间件添加的两种方式(接口耗时中间件)
- 使用wrap_fn + 闭包实现
- 使用warp + struct实现
- 中间件调用顺序
- actix自带的接口耗时中间件
- 鉴权中间件
Actix-web定义中间件(middleware)记录接口耗时
actix-web的官网关于中间件的介绍如下
- https://actix.rs/docs/middleware/
这里使用的是最新版的actix-web,旧版本的可能接口不太一样
# actix-web
actix-rt = "2.6.0"
actix-web = "4.0.0"
中间件简介
我们添加的中间件能干什么?我们用一段代码来观察一下
下面是官方提供的中间件的定义方式之一,我们可以看到闭包里面有两个参数req
和srv
- 其中
req(actix_web::dev::ServiceRequest)
就是请求了,你可以通过它来获取请求的各种属性,比如请求的路径req.path()
srv
是一个&mut dyn actix_web::dev::Service
类型的参数,代表服务处理程序。通过调用srv.call(req)
,我们将请求传递给实际的服务处理程序进行处理。这里其实使用的是装饰器模式
,这使得我们能在具体service方法调用前后做一些操作。官网上写到可以添加这些操作- Pre-process the Request:在请求时做前置处理
- Post-process a Response:在响应时进行后置处理
- Modify application state:修改state。state是我
actix-web
在整个调用链路中的上下文,可以用来存储我们自己想要保存的数据 - Access external services (redis, logging, sessions):可以访问外部的服务,例如redis等等
use actix_web::{dev::Service as _, web, App};
use futures_util::future::FutureExt;#[actix_web::main]
async fn main() {let app = App::new().wrap_fn(|req, srv| {// Pre-process the Requestprintln!("Hi from start. You requested: {}", req.path());srv.call(req).map(|res| {// Post-process a Responseprintln!("Hi from response");res})}).route("/index.html",web::get().to(|| async { "Hello, middleware!" }),);
}
中间件添加的两种方式(接口耗时中间件)
上述添加中间件的方式其实是通过wrap_fn
来实现的,我们可以通过传入闭包的方式完成我们中间件的逻辑
但是一般我们中间件处理的逻辑可能很多,在闭包中修改会显得很冗余,我们还可以通过方法warp
传入一个实现了 Service trait and Transform trait 的struct
,这样就会调用我们实现好的方法
在实现前我们需要先添加一些依赖
# actix-web
actix-rt = "2.6.0"
actix-web = "4.0.0"
# 提供对异步编程的支持和工具
futures-util = "0.3"
使用wrap_fn + 闭包实现
use actix_web::{dev::Service as _, web, App};
use futures_util::future::FutureExt;
use std::time::{Duration, Instant};#[actix_web::main]
async fn main() {let app = App::new().wrap_fn(|req, srv| {let start_time = Instant::now();let path = req.path().to_owned();let fut = srv.call(req);async move {let res = fut.await;let elapsed_time = start_time.elapsed();println!("Request to {} took {:?}",path,elapsed_time);res}}).route("/index.html",web::get().to(|| async { "Hello, middleware!" }),);
}
这样就能打印接口耗时的日志了
Request to /index.html took 381.325909ms
使用warp + struct实现
使用struct
需要实现两个trait
:Transform
和Service
// 中间件 => 打印接口耗时use std::{future::{ready, Ready}, time::Instant};use actix_web::{dev::{forward_ready, Service, ServiceRequest, ServiceResponse, Transform},Error,
};
use futures_util::future::LocalBoxFuture;// There are two steps in middleware processing.
// 1. Middleware initialization, middleware factory gets called with
// next service in chain as parameter.
// 2. Middleware's call method gets called with normal request.
pub struct Timed;// Middleware factory is `Transform` trait
// `S` - type of the next service
// `B` - type of response's body
impl<S, B> Transform<S, ServiceRequest> for Timed
whereS: Service<ServiceRequest, Response = ServiceResponse<B>, Error = Error>,S::Future: 'static,B: 'static,
{type Response = ServiceResponse<B>;type Error = Error;type InitError = ();type Transform = TimedMiddleware<S>;type Future = Ready<Result<Self::Transform, Self::InitError>>;fn new_transform(&self, service: S) -> Self::Future {ready(Ok(TimedMiddleware { service }))}
}pub struct TimedMiddleware<S> {service: S,
}impl<S, B> Service<ServiceRequest> for TimedMiddleware<S>
whereS: 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>>;forward_ready!(service);fn call(&self, req: ServiceRequest) -> Self::Future {let start_time = Instant::now();let path = req.path().to_owned();let method = req.method().to_string();let remote_addr = req.connection_info().peer_addr().unwrap_or("unknown").to_string();let version = format!("{:?}", req.version()); // 使用 format! 宏转换版本号为字符串let headers = req.headers().clone();println!("{}", "1. Pre-process the Request");let fut = self.service.call(req);Box::pin(async move {let res = fut.await?;let elapsed = start_time.elapsed();let status = res.status();let content_length = res.headers().get(actix_web::http::header::CONTENT_LENGTH).and_then(|v| v.to_str().ok()).unwrap_or("-");let user_agent = headers.get(actix_web::http::header::USER_AGENT).and_then(|v| v.to_str().ok()).unwrap_or("-");println!("{}", "2. Post-process a Response")println!("{} {} {} {} {} {} {} time took [{:.6}] ms",remote_addr,method,path,version,status.as_u16(),content_length,user_agent,elapsed.as_millis());Ok(res)})}
}
在主程序中添加
use actix_web::{dev::Service as _, web, App};
use futures_util::future::FutureExt;
use std::time::{Duration, Instant};#[actix_web::main]
async fn main() {let app = App::new().wrap_fn(|req, srv| {println!("{}", "2. Pre-process the Request")let start_time = Instant::now();let path = req.path().to_owned();let fut = srv.call(req);async move {let res = fut.await;let elapsed_time = start_time.elapsed();println!("{}", "3. Post-process a Response")println!("Request to {} took {:?}",path,elapsed_time);res}}).wrap(Timed).route("/index.html",web::get().to(|| async { "Hello, middleware!" }),);
}
打印情况
1. Pre-process the Request
2. Pre-process the Request
3. Post-process a Response
Request to /index.html took 70.694224ms
4. Post-process a Response
127.0.0.1 GET /index.html HTTP/1.1 200 - Apifox/1.0.0 (https://www.apifox.cn) time took [70] ms
中间件调用顺序
如果我们有多个中间件,调用顺序可以从官方上看到这样一句话
Warning: if you use
wrap()
orwrap_fn()
multiple times, the last occurrence will be executed first.也就是后面添加的中间件会先执行
笔者根据上面添加的中间件,对于前置和后置处理我们可以总结出调用顺序
- 前置比后置处理先调用
- 前置处理是后添加的先执行
- 后置处理按照中间件添加的属性进行执行
actix自带的接口耗时中间件
其实在actix中自带了接口耗时的记录,我们只需要指定日志,并启用就可以看到了
# 日志相关
log = "0.4.0"
env_logger = "0.10.0"
use actix_web::{dev::Service as _, web, App, middleware};
use futures_util::future::FutureExt;
use std::time::{Duration, Instant};
use log::info;#[actix_web::main]
async fn main() {// 初始化日志init_logger();let app = App::new()// 日志中间件.wrap(middleware::Logger::default()).wrap_fn(|req, srv| {println!("{}", "2. Pre-process the Request")let start_time = Instant::now();let path = req.path().to_owned();let fut = srv.call(req);async move {let res = fut.await;let elapsed_time = start_time.elapsed();println!("{}", "3. Post-process a Response")println!("Request to {} took {:?}",path,elapsed_time);res}}).wrap(Timed).route("/index.html",web::get().to(|| async { "Hello, middleware!" }),);
}fn init_logger() {use env_logger::fmt::Color;use env_logger::Env;use log::LevelFilter;let env = Env::default().filter_or("MY_LOG_LEVEL", "debug");// 设置日志打印格式env_logger::Builder::from_env(env).format(|buf, record| {let level_color = match record.level() {log::Level::Error => Color::Red,log::Level::Warn => Color::Yellow,log::Level::Info => Color::Green,log::Level::Debug | log::Level::Trace => Color::Cyan,};let mut level_style = buf.style();level_style.set_color(level_color).set_bold(true);let mut style = buf.style();style.set_color(Color::White).set_dimmed(true);writeln!(buf,"{} {} [{}] {}",Local::now().format("%Y-%m-%d %H:%M:%S"),level_style.value(record.level()),style.value(record.module_path().unwrap_or("<unnamed>")),record.args())}).filter(None, LevelFilter::Debug).init();info!("env_logger initialized.");
}
日志打印
2023-08-24 16:06:14 INFO [teacher_service] env_logger initialized.
2023-08-24 16:06:14 INFO [actix_server::builder] starting 2 workers
2023-08-24 16:06:14 INFO [actix_server::server] Actix runtime found; starting in Actix runtime
1. Pre-process the Request
2. Pre-process the Request
2023-08-24 16:06:50 INFO [teacher_service::my_middleware::auth] 2. Hi from start. You requested: /teacher
2023-08-24 16:06:50 INFO [teacher_service] 1. Hi from start. You requested: /teacher
2023-08-24 16:06:50 INFO [teacher_service] 2. Hi from response
2023-08-24 16:06:50 INFO [teacher_service::my_middleware::auth] 1. Hi from response
3. Post-process a Response
Request to /teacher took 355.839222ms
4. Post-process a Response
2023-08-24 16:06:50 INFO [teacher_service::my_middleware::timedMiddleware] 127.0.0.1 GET /teacher HTTP/1.1 200 - Apifox/1.0.0 (https://www.apifox.cn) time took [355] ms
2023-08-24 16:06:50 INFO [actix_web::middleware::logger] 127.0.0.1 "GET /teacher HTTP/1.1" 200 191 "-" "Apifox/1.0.0 (https://www.apifox.cn)" 0.355607
最后一行就是actix日志记录请求的调用情况,最后一个参数就是调用时间,单位是秒
鉴权中间件
我们用相同的思路,写一个鉴权的中间件,这里具体的校验规则读者可以实现一下
use std::future::{ready, Ready};use actix_web::{dev::{forward_ready, Service, ServiceRequest, ServiceResponse, Transform},error,http::header::HeaderValue,middleware::ErrorHandlerResponse,Error, HttpResponse,
};
use futures_util::{future::{self, LocalBoxFuture},FutureExt,
};
use log::info;// There are two steps in middleware processing.
// 1. Middleware initialization, middleware factory gets called with
// next service in chain as parameter.
// 2. Middleware's call method gets called with normal request.
pub struct Auth;// Middleware factory is `Transform` trait
// `S` - type of the next service
// `B` - type of response's body
impl<S, B> Transform<S, ServiceRequest> for Auth
whereS: 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 }))}
}pub struct AuthMiddleware<S> {service: S,
}impl<S, B> Service<ServiceRequest> for AuthMiddleware<S>
whereS: 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>>;forward_ready!(service);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();let token = req.headers().get("token").unwrap_or(&value);token.len() > 0 || req.path().to_string() == "/login"
}
接下来我们在启动的App上加上中间件,这里我们要注意⚠️,如果我们有很多中间件,我们肯定是想要我们的鉴权中间件先执行的,这样如果鉴权没有过,就不执行后面中间件的逻辑
根据官方的提示:后添加的中间件会先执行。我们应该把鉴权中间件放到最后面的位置
use actix_web::{dev::Service as _, web, App, middleware};
use futures_util::future::FutureExt;
use std::time::{Duration, Instant};
use log::info;#[actix_web::main]
async fn main() {// 初始化日志init_logger();let app = App::new()// 日志中间件.wrap(middleware::Logger::default()).wrap_fn(|req, srv| {println!("{}", "2. Pre-process the Request")let start_time = Instant::now();let path = req.path().to_owned();let fut = srv.call(req);async move {let res = fut.await;let elapsed_time = start_time.elapsed();println!("{}", "3. Post-process a Response")println!("Request to {} took {:?}",path,elapsed_time);res}}).wrap(Timed)// 添加自己中间件的路径.wrap(my_middleware::auth::Auth).route("/index.html",web::get().to(|| async { "Hello, middleware!" }),);
}fn init_logger() {use env_logger::fmt::Color;use env_logger::Env;use log::LevelFilter;let env = Env::default().filter_or("MY_LOG_LEVEL", "debug");// 设置日志打印格式env_logger::Builder::from_env(env).format(|buf, record| {let level_color = match record.level() {log::Level::Error => Color::Red,log::Level::Warn => Color::Yellow,log::Level::Info => Color::Green,log::Level::Debug | log::Level::Trace => Color::Cyan,};let mut level_style = buf.style();level_style.set_color(level_color).set_bold(true);let mut style = buf.style();style.set_color(Color::White).set_dimmed(true);writeln!(buf,"{} {} [{}] {}",Local::now().format("%Y-%m-%d %H:%M:%S"),level_style.value(record.level()),style.value(record.module_path().unwrap_or("<unnamed>")),record.args())}).filter(None, LevelFilter::Debug).init();info!("env_logger initialized.");
}
这样当我们鉴权失败后,后续的中间件就不会执行了(下面笔者写了一个接口)
鉴权成功
日志也符合预期