この記事で使用されているactix-webのバージョンは4.1.0です。
通常、認証インターセプトを実装する場合、ミドルウェアの使用を検討します。actix -web-httpauthなどですが、使用するのに十分な柔軟性がありません。たとえば、インターフェイスをインターセプトする必要がない場合や、ログイン時と非ログイン時に2つの異なる応答が返される場合があります。
この記事では、actix-webのHandler<Args>とFromRequestから楽しいメソッドを知ることができます。
原則ベース
ヒント:原則をすでに知っている場合、または原則を知ることを期待していない場合は、このセクションをスキップできます
actix-webでは、ルート設定ハンドラーメソッドは次のように定義されています。
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
}
handler_service
メソッドのみが呼び出され、パラメーターハンドラーの制約はHandler<Args>
次のように定義されます。
pub trait Handler<Args>: Clone + 'static {
type Output;
type Future: Future<Output = Self::Output>;
fn call(&self, args: Args) -> Self::Future;
}
ここで、Argsはn個のパラメーターを表すと推測できます。この定義のすぐ下に、上記のhandler_service
メソッド。
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))
}
}))
}
Args
の制約からわかるFromRequest
ように、パラメータとして使用できるを実装するだけで済みます。FromRequest
つまり、Handleメソッドの任意のパラメーター位置に、必要なパラメーター名とタイプを書き込むことができます。
例:パラメーターでHttpメソッドを取得する場合は、次のようにします。
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
その後、パラメータが正常に抽出された場合はhandler.call
、メソッドを呼び出してインターフェイスを実行します。失敗した場合は、で定義されたエラーがFromRequest
返されるため、パラメータ抽出が失敗した後に返される内容を自由に定義できます。
不过看到这里还是会有疑问,这个 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()
}
もちろん、これは今は有効になりません。UserData
実装FromRequest
。検証としてJWTを使用します。これは重要ではないため、ここでは無視してください。完全なコードはオープンソースリポジトリに配置されます。
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"))
}
})
}
}
これを実装した後、今のところインターフェースは問題ありません。
ログインするのではなくログインする場合は、インターフェイスにアクセスできますが、インターフェイスによって返されるコンテンツが異なるため、このメソッドを使用して次のことを実行することもできます。
/// 无需登陆
/// 登陆前后拿到的数据不完全相同
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")
}
}
これは、actix-web内で状況を実装Option<T: FromRequest>
するためです。
さらに、インターフェースでユーザーIDを使用する必要はないが、それでもインターセプトしたい場合は、パラメーターを書き込むだけで使用しないという非常に簡単です。
どのように、それは非常に柔軟ですか?:)
インターフェーステスト
ログインする
# 登陆用户名为 123 密码为 456 的账号,返回值: "Bearer ..."
curl http://127.0.0.1:8000/login -H "Content-Type:application/json" -d '{"id":123,"pwd":"456"}'
# 导出返回值到环境变量以便后续使用
export token="Bearer ..."
情報を教えてもらう
# 携带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"
公開情報を入手する
# 未携带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"