La versión actix-web utilizada en este artículo es 4.1.0
Por lo general, cuando implementamos la interceptación de autenticación, pensaremos en usar middleware. Como actix-web-httpauth , pero no es lo suficientemente flexible para usar. Por ejemplo, no es necesario interceptar una interfaz, o se devuelven dos respuestas diferentes al iniciar sesión y al no iniciar sesión.
Este artículo lo lleva a conocer un método divertido de Handler<Args> y FromRequest de actix-web .
Principio base
Sugerencia: si ya conoce el principio o no espera conocerlo, puede omitir esta sección.
En actix-web, el método del controlador de configuración de ruta se define de la siguiente manera:
pub fn to<F, Args>(self, handler: F) -> Self
where
F: Handler<Args>,
Args: FromRequest + 'static,
F::Output: Responder + 'static,
{
self.service = handler_service(handler);
self
}
Solo se llama al handler_service
método y la restricción del controlador de parámetros es Handler<Args>
, que se define de la siguiente manera:
pub trait Handler<Args>: Clone + 'static {
type Output;
type Future: Future<Output = Self::Output>;
fn call(&self, args: Args) -> Self::Future;
}
Aquí podemos suponer que Args representa n parámetros. Inmediatamente debajo de esta definición, podemos ver el handler_service
método :
pub(crate) fn handler_service<F, Args>(handler: F) -> BoxedHttpServiceFactory
where
F: Handler<Args>,
Args: FromRequest,
F::Output: Responder,
{
boxed::factory(fn_service(move |req: ServiceRequest| {
let handler = handler.clone();
async move {
let (req, mut payload) = req.into_parts();
let res = match Args::from_request(&req, &mut payload).await {
Err(err) => HttpResponse::from_error(err),
Ok(data) => handler
.call(data)
.await
.respond_to(&req)
.map_into_boxed_body(),
};
Ok(ServiceResponse::new(req, res))
}
}))
}
Como podemos ver Args
en las restricciones de , solo necesitamos implementar FromRequest
, que se pueden usar como parámetros. Es decir FromRequest
, puede escribir el nombre del parámetro y el tipo que desee en cualquier posición del parámetro en el método Handle.
Por ejemplo: cuando desee obtener el método Http en el parámetro, puede hacer esto:
async fn hander(method: Method) -> HttpResponse {...}
/// 因为 actix-web 内部 Method 实现了 FromRequest
impl FromRequest for Method {
type Error = Infallible;
type Future = Ready<Result<Self, Self::Error>>;
fn from_request(req: &HttpRequest, _: &mut Payload) -> Self::Future {
ok(req.method().clone())
}
}
from_request
Después de eso, si los parámetros se extraen con éxito, llame handler.call
al método para ejecutar su interfaz, y si falla, FromRequest
devolverá el Error definido en , para que pueda definir libremente qué se devuelve después de que falla la extracción de parámetros.
不过看到这里还是会有疑问,这个 Args
到底传入的是什么?
让我们继续往下看,会看到一个宏定义:
macro_rules! factory_tuple ({ $($param:ident)* } => {
impl<Func, Fut, $($param,)*> Handler<($($param,)*)> for Func
where
Func: Fn($($param),*) -> Fut + Clone + 'static,
Fut: Future,
{
type Output = Fut::Output;
type Future = Fut;
#[inline]
#[allow(non_snake_case)]
fn call(&self, ($($param,)*): ($($param,)*)) -> Self::Future {
(self)($($param,)*)
}
}
});
factory_tuple! {}
factory_tuple! { A }
factory_tuple! { A B }
factory_tuple! { A B C }
factory_tuple! { A B C D }
factory_tuple! { A B C D E }
factory_tuple! { A B C D E F }
factory_tuple! { A B C D E F G }
factory_tuple! { A B C D E F G H }
factory_tuple! { A B C D E F G H I }
factory_tuple! { A B C D E F G H I J }
factory_tuple! { A B C D E F G H I J K }
factory_tuple! { A B C D E F G H I J K L }
如果你不了解宏的话,可以看看 The Book 还有 rust by example 这里不做赘述了。
可以看到这里一揽子实现了各个类型,数量范围 [0, 12],也就是把 Args 为 (), (A), (A, B)... 等都实现了。所以当你的接口超过12个参数的时候就会报错(应该不会有接口超过12个参数吧?)。
回答上面的问题: Args
就是 一堆 tuple
,个数从 0 到 12。
那么又有新的问题来了,在上面的例子中 Method 实现了 FromRequest
,但是在 handler_service
方法中,from_request
只被调用了一次,不应该有几个参数就调用几次吗?
这时候就需要再次看源码了,在 FromRequest 文档中点击右上角的 [source] (确保你的文档打开的版本是4.1.0),找到 312 行,会发现一个隐藏的模块 tuple_from_req
,代码有点多,这里就只贴最关键的部分:
/// FromRequest implementation for tuple
#[allow(unused_parens)]
impl<$($T: FromRequest + 'static),+> FromRequest for ($($T,)+)
{
type Error = Error;
type Future = $fut<$($T),+>;
fn from_request(req: &HttpRequest, payload: &mut Payload) -> Self::Future {
$fut {
$(
$T: ExtractFuture::Future {
fut: $T::from_request(req, payload)
},
)+
}
}
}
能看到这里,就已经水落石出了,原来是又用了个宏来帮tuple一个个调用 from_request
方法。所以在 handler_service
里的 Args
实际上是一个 tuple
, 调用的 from_request
方法是从这里开始。
身份拦截的实现
简单定义一个用户数据,包含一个字段,代表用户id。
pub struct UserData {
pub id: i32,
}
在我们登陆之后需要拿到拦截未登陆的用户的时候只需要在参数上写上它就好了:
async fn get_info(user: UserData) -> impl Responder {
HttpResponse::Ok().finish()
}
Por supuesto, esto no tendrá efecto ahora, necesitamos UserData
implementarlo FromRequest
, usamos JWT como verificación, porque no es el punto, ignórelo aquí, el código completo se colocará en el repositorio de código abierto.
use actix_web::{dev::Payload, error, Error, FromRequest, HttpRequest};
use std::future::{ready, Ready};
impl FromRequest for UserData {
type Error = Error;
type Future = Ready<Result<Self, Self::Error>>;
fn from_request(req: &HttpRequest, _payload: &mut Payload) -> Self::Future {
ready({
let auth = req.headers().get("Authorization");
if let Some(val) = auth {
let token = val.to_str().unwrap().split("Bearer ").collect::<Vec<&str>>().pop().unwrap();
let result = auth::validate_token(token);
match result {
Ok(data) => Ok(UserData { id: data.claims.id }),
Err(e) => {
eprintln!("{}", e);
Err(error::ErrorBadRequest("Invalid Authorization"))
}
}
} else {
Err(error::ErrorUnauthorized("Authorization Not Found"))
}
})
}
}
Después de implementar esto, la interfaz ahora no es un problema.
Cuando queremos iniciar sesión y no iniciar sesión, podemos acceder a una interfaz, pero el contenido devuelto por la interfaz es diferente, también podemos usar este método para lograr:
/// 无需登陆
/// 登陆前后拿到的数据不完全相同
async fn get_public_info(user: Option<UserData>) -> impl Responder {
if let Some(user) = user {
HttpResponse::Ok().json(format!("public data with {}", user.id))
} else {
HttpResponse::Ok().json("public data")
}
}
Esto se debe a que, dentro de actix-web, implementa Option<T: FromRequest>
la situación.
Además, cuando no necesita usar la identificación de usuario en la interfaz, pero aún desea interceptar, entonces es muy simple, simplemente escriba los parámetros y no los use.
¿Cómo, es muy flexible? :)
prueba de interfaz
acceso
# 登陆用户名为 123 密码为 456 的账号,返回值: "Bearer ..."
curl http://127.0.0.1:8000/login -H "Content-Type:application/json" -d '{"id":123,"pwd":"456"}'
# 导出返回值到环境变量以便后续使用
export token="Bearer ..."
consiguiendo información
# 携带token,返回值为200
curl http://127.0.0.1:8000/info -v -X POST -H "Authorization:$token"
# Headers 不包含 Authorization 返回 401 Unauthorized 未验证
curl http://127.0.0.1:8000/info -v -X POST
# 输入错误的token,返回 400 Bad Request 返回 错误请求
curl http://127.0.0.1:8000/info -v -X POST -H "Authorization:Bearer ErrorToken"
Obtener información pública
# 未携带token请求,返回公共数据:“public data" 不包含个人信息。
curl http://127.0.0.1:8000/public -v -X POST
# 携带了正确的token,返回带有个人数据的公共数据 ”public data with 123“
curl http://127.0.0.1:8000/public -v -X POST -H "Authorization:$token"