actix-web more flexible authentication interception implementation

The actix-web version used in this article is 4.1.0

Usually when implementing authentication interception, we will think of using middleware. Such as actix-web-httpauth , but it is not flexible enough to use. For example, an interface does not need to be intercepted, or two different responses are returned when logging in and not logging in.

This article takes you to know a fun method from Handler<Args> and FromRequest of actix-web .

Principle basis

Tip: If you already know the principle, or do not expect to know the principle, you can skip this section

In actix-web, the route setting handler method is defined as follows:

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
}

Only the handler_servicemethod , and the constraint of the parameter handler is Handler<Args>, which is defined as follows:

pub trait Handler<Args>: Clone + 'static {
    type Output;
    type Future: Future<Output = Self::Output>;

    fn call(&self, args: Args) -> Self::Future;
}

Here we can guess that Args represents n parameters. Immediately below this definition, we can see the handler_servicemethod :

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))
        }
    }))
}

As we can see Argsfrom the constraints of , we only need to implement FromRequest, which can be used as parameters. That is to say, FromRequestyou can write the parameter name and type you want in any parameter position in the Handle method.

For example: When you want to get the Http Method in the parameter, you can do this:

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_requestAfter that, if the parameters are successfully extracted, call handler.callthe method to execute your interface, and if it fails, it will FromRequestreturn the Error defined in , so you can freely define what is returned after the parameter extraction fails.

不过看到这里还是会有疑问,这个 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()
}

Of course, this will not take effect now, and we need to UserDataimplement it FromRequest. We use JWT as the verification, because it is not the point, ignore it here, and the complete code will be placed on the open source repository.

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"))
            }
        })
    }
}

After implementing this, the interface just now is no problem.

When we want to log in and not log in, we can access an interface, but the content returned by the interface is different, we can also use this method to achieve:

/// 无需登陆
/// 登陆前后拿到的数据不完全相同
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")
    }
}

This is because inside actix-web, you implement Option<T: FromRequest>the situation.

In addition, when you do not need to use the user id in the interface, but still want to intercept, then it is very simple, just write the parameters and not use it.

How, is it very flexible? :)

interface test

login

# 登陆用户名为 123 密码为 456 的账号,返回值: "Bearer ..."
curl http://127.0.0.1:8000/login -H "Content-Type:application/json" -d '{"id":123,"pwd":"456"}'

# 导出返回值到环境变量以便后续使用
export token="Bearer ..."

getting information

# 携带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"

Get public information

# 未携带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"

Warehouse Address

actix-handler-args-tutorial

Guess you like

Origin juejin.im/post/7121239259985477668