< 返回版块

祐祐 发表于 2021-10-13 22:19

Tags:axum, 中间件, Header, 权限控制, rocket

有三个冷门的问题(但应该要有解...)

  1. [已解決]axum预设http标头 "content-type" 预设是 "text/plain" 我想实现将所有等于 "text/plain" 高效的自动转换成 "text/plain; charset=utf-8" 我应该如何实现。 (目前想到使用中间件) (注明一下: 我知道可以使用 async fn index() -> (HeaderMap, String) {...} 但是这超级麻烦,想一次控制全部的标头也方便日后更改。
//ex: 
async fn handler() -> String {}       // 预设是"text/plain"
async fn handler() -> &'static str {} // 预设是"text/plain"
  1. axum中的tower中间件应该如何使用,axum官方给的中间件预设有一些奇怪的操作,很不解干嘛用的,例如官方示例axum:writing-your-own-middleware 这是什么,这两行在我每次响应时都会做一次clone self.inner.clone(); 怎么会在axum这里看到这样奇怪的操作,这不是浪费资源吗? 我映像中这样的操作有成本的除非是Rc、Arc...
// best practice is to clone the inner service like this
// see https://github.com/tower-rs/tower/issues/547 for details
let clone = self.inner.clone();
let mut inner = std::mem::replace(&mut self.inner, clone);
  1. 我一直在想axum 怎么做权限控制,我一直希望有个能像rocket: 权限控制作法一样或是替代品,axum有吗?如果真的没有有人可以提供我解决方法吗?

评论区

写评论
Bai-Jinlin 2021-11-12 13:28

下面的伙计已经写了解决方法,这里写一下我的方法。

  1. 的话tower_http有现成的中间件SetResponseHeaderLayer,可以替换头部。
  2. 的话其实大部分场景里tower和tower_http都有现成的中间件可以使用,基本不用自己写。
  3. 的话假如用Authorization头鉴权的话也可以用tower_http里面的RequireAuthorizationLayer中间件,并且自己定义一下鉴权的规则。
use axum::body::{box_body, BoxBody};
use axum::extract::Extension;
use axum::handler::Handler;
use axum::http::{header, Request, Response, Uri};
use axum::response::{IntoResponse, Redirect};
use axum::{routing::get, Router};
use headers::{HeaderName, HeaderValue};
use tower_http::auth::{AuthorizeRequest, RequireAuthorizationLayer};
use tower_http::set_header::SetResponseHeaderLayer;

#[derive(Clone)]
struct MyAuth {
    n: i32,
}
impl AuthorizeRequest for MyAuth {
    type Output = i32;
    type ResponseBody = BoxBody;
    fn authorize<B>(&mut self, request: &Request<B>) -> Option<Self::Output> {
        #[rustfmt::skip]
        let i=request.headers().get(header::AUTHORIZATION).map(|header|{
            header.to_str().ok().map(|s|s.parse::<i32>().ok()).flatten()
        }).flatten()?;
        (i % self.n == 0).then(|| i)
    }
    fn on_authorized<B>(&mut self, request: &mut Request<B>, output: Self::Output) {
        request.extensions_mut().insert(output);
    }
    fn unauthorized_response<B>(&mut self, _request: &Request<B>) -> Response<Self::ResponseBody> {
        // use axum::body::{Bytes, Full};
        // let buf = Bytes::from("unauthorized!");
        // Response::builder()
        //     .status(StatusCode::UNAUTHORIZED)
        //     .body(box_body(Full::new(buf)))
        //     .unwrap()
        Redirect::to(Uri::from_static("/login"))
            .into_response()
            .map(|b| box_body(b))
    }
}
async fn handle(Extension(i): Extension<i32>) -> String {
    i.to_string()
}

#[tokio::main]
async fn main() {
    let app = Router::new()
        .route(
            "/",
            get(handle.layer(RequireAuthorizationLayer::custom(MyAuth { n: 2 }))),
        )
        .route("/login", get(|| async { "login" }))
        .layer(SetResponseHeaderLayer::<_, BoxBody>::overriding(
            HeaderName::from_static("content-type"),
            HeaderValue::from_static("text/plain; charset=utf-8"),
        ));

    axum::Server::bind(&"127.0.0.1:8000".parse().unwrap())
        .serve(app.into_make_service())
        .await
        .unwrap();
}

这里的话首先对content-type全部替换了一遍,请求/的时候会对Authorization头后面的数字进行判断是否能被2整除,可以的话用handle的Extension extractor提取出来成功鉴权的数字并响应,不能的话就重定向到/login

c5soft 2021-10-18 21:01

再来说说第二个问题,axum文档中给出的Middleware完整实例源码:

use axum::{
    body::{Body, BoxBody},
    handler::get,
    http::{Request, Response},
    Router,
};
use futures::future::BoxFuture;
use tower::{Service, layer::layer_fn};
use std::task::{Context, Poll};

#[derive(Clone)]
struct MyMiddleware<S> {
    inner: S,
}

impl<S, ReqBody, ResBody> Service<Request<ReqBody>> for MyMiddleware<S>
where
    S: Service<Request<ReqBody>, Response = Response<ResBody>> + Clone + Send + 'static,
    S::Future: Send + 'static,
    ReqBody: Send + 'static,
    ResBody: Send + 'static,
{
    type Response = S::Response;
    type Error = S::Error;
    type Future = BoxFuture<'static, Result<Self::Response, Self::Error>>;

    fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
        self.inner.poll_ready(cx)
    }

    fn call(&mut self, mut req: Request<ReqBody>) -> Self::Future {
        println!("`MyMiddleware` called!");

        // best practice is to clone the inner service like this
        // see https://github.com/tower-rs/tower/issues/547 for details
        let clone = self.inner.clone();
        let mut inner = std::mem::replace(&mut self.inner, clone);

        Box::pin(async move {
            let res: Response<ResBody> = inner.call(req).await?;

            println!("`MyMiddleware` received the response");

            Ok(res)
        })
    }
}

let app = Router::new()
    .route("/", get(|| async { /* ... */ }))
    .layer(layer_fn(|inner| MyMiddleware { inner }));

其中这两行:

        let clone = self.inner.clone();
        let mut inner = std::mem::replace(&mut self.inner, clone);

是所有权处理,将对inner的借用,变成对inner的拥有。因为下面async move需要将inner所有权完全转移到future代码片段中。clone是浅复制,mem:replace是深复制。通过这两行,产生一个与self.inner内容一模一样的,拥有所有权的变量,取名叫inner。

c5soft 2021-10-18 10:34

服务端启动后可以通过curl命令行测试:

curl http://127.0.0.1:3000
<html><body><h1>Welcome to Home Page</h1></body></html>

curl http://127.0.0.1:3000/login
<h1>Login Page</h1>

curl http://127.0.0.1:3000/api/users
返回303状态码与调转的LOCATION,没有内容。增加-L参数自动执行跳转。

curl -L http://127.0.0.1:3000/api/users
<h1>Login Page</h1>


curl -H "authorization:secret" http://127.0.0.1:3000/api/users
api_users

curl -L -H "authorization:bad_secret" http://127.0.0.1:3000/api/users
<h1>Login Page</h1>

在http请求头部提供authorization:secret的情况下可以访问/api下面的页面,否则跳转到/login。

另外说一下,前几天我写的axum_utf8.rs改一下可以减少对bytes与http-body的外部依赖,axum内部公开了对bytes与http-body的引用,axum::body=crate::http_body, axum::body::bytes=crate::bytes。

c5soft 2021-10-18 10:01

亲,权限控制实例代码新鲜出炉:

use async_trait::async_trait;
use axum::{
    body::{Body, BoxBody},
    extract::{extractor_middleware, FromRequest, RequestParts},
    handler::{get, post},
    http::{StatusCode,Request,Uri},
    response::{Html,IntoResponse,Redirect},
    routing::BoxRoute,
    Router,
};

// An extractor that performs authorization.
struct RequireAuth;

#[async_trait]
impl<B> FromRequest<B> for RequireAuth
where
    B: Send,
{
    type Rejection = Redirect; //StatusCode;

    async fn from_request(req: &mut RequestParts<B>) -> Result<Self, Self::Rejection> {
        let auth_header = req
            .headers()
            .and_then(|headers| headers.get(axum::http::header::AUTHORIZATION))
            .and_then(|value| value.to_str().ok());

        if let Some(value) = auth_header {
            if value == "secret" {
                return Ok(Self);
            }
        }

        //Err(StatusCode::UNAUTHORIZED)
        Err(Redirect::to(Uri::from_static("/login")))
    }
}

async fn home() -> impl IntoResponse {
    Html("<html><body><h1>Welcome to Home Page</h1></body></html>")
}

async fn login() -> impl IntoResponse {
    Html("<h1>Login Page</h1>")
}

async fn api_users() ->impl IntoResponse  {
    "api_users"
}

async fn api_job() -> impl IntoResponse  {
    "job".to_string()
}

#[tokio::main]
async fn main() {
    fn api_routes() -> Router<BoxRoute> {
        Router::new()
            .route("/users", get(api_users))
            .route("/job", post(api_job))
            .layer(extractor_middleware::<RequireAuth>())
            .boxed()
    }

    // build our application with a single route
    let app = Router::new()
        .route("/", get(home))
        .route("/login", get(login))
        // The extractor will run before all routes
        .nest("/api", api_routes());

    // run it with hyper on localhost:3000
    axum::Server::bind(&"0.0.0.0:3000".parse().unwrap())
        .serve(app.into_make_service())
        .await
        .unwrap();
}

服务器端提供两类页面,一类是公开访问页面,不需要授权,如主页,登录页面;另一类是获得授权才能访问的页面,实例代码中放置在uri为/api/*下面。服务将检查Request头部的authorization字段, 审核通过返回需要的页面,审核失败调转到登录页面。

作者 祐祐 2021-10-15 22:22

好的感谢你,第一个方法已经成功。 但是对于权限管理还是一头雾水...。

--
👇
c5soft: 第一个问题,我也碰到了,直接贴出我写的源代码axum_utf8.rs

除了Plain,axum_utf8.rs中顺便也增加了utf8版的Html。

第二个问题与第三个问题需要花时间去研读tower/tower-http, 都是如何使用中间件的问题,读懂tower,一切明了。

c5soft 2021-10-14 09:19

第一个问题,我也碰到了,直接贴出我写的源代码axum_utf8.rs

use axum::http::{
    header::{self, HeaderValue},
    Response,
};
use axum::response::IntoResponse;
use bytes::Bytes;
use http_body::Full;
use std::convert::Infallible;

#[derive(Clone, Copy, Debug)]
pub struct Plain<T>(pub T);

impl<T> IntoResponse for Plain<T>
where
    T: Into<Full<Bytes>>,
{
    type Body = Full<Bytes>;
    type BodyError = Infallible;

    fn into_response(self) -> Response<Self::Body> {
        let mut res = Response::new(self.0.into());
        res.headers_mut().insert(
            header::CONTENT_TYPE,
            HeaderValue::from_static("text/plain;charset=utf-8"),
        );
        res
    }
}

impl<T> From<T> for Plain<T> {
    fn from(inner: T) -> Self {
        Self(inner)
    }
}


#[derive(Clone, Copy, Debug)]
pub struct Html<T>(pub T);

impl<T> IntoResponse for Html<T>
where
    T: Into<Full<Bytes>>,
{
    type Body = Full<Bytes>;
    type BodyError = Infallible;

    fn into_response(self) -> Response<Self::Body> {
        let mut res = Response::new(self.0.into());
        res.headers_mut().insert(
            header::CONTENT_TYPE,
            HeaderValue::from_static("text/html;charset=utf-8"),
        );
        res
    }
}

impl<T> From<T> for Html<T> {
    fn from(inner: T) -> Self {
        Self(inner)
    }
}

需要在Cargo.toml增加对bytes于http-body的依赖,main.rs中的写法:

mod axum_utf8;
use axum_utf8::Plain;

use axum::{handler::get, Router};
use std::net::SocketAddr;

#[tokio::main]
async fn main() {
    // build our application with a route
    let app = Router::new().route("/", get(hi));

    // run it
    let addr = SocketAddr::from(([127, 0, 0, 1], 3000));
    println!("listening on {}", addr);
    axum::Server::bind(&addr)
        .serve(app.into_make_service())
        .await
        .unwrap();
}

// async fn handler() -> Html<&'static str> {
//     Html("<h1>Hello, World! 欢迎</h1>")
// }

// basic handler that responds with a static string
async fn hi() -> Plain<&'static str> {
    Plain("Hello, World! 你好,东方!")
}

除了Plain,axum_utf8.rs中顺便也增加了utf8版的Html。

第二个问题与第三个问题需要花时间去研读tower/tower-http, 都是如何使用中间件的问题,读懂tower,一切明了。

1 共 6 条评论, 1 页