Rust in Everything
基于 tower-http 0.7.1

校验与鉴权

主线位置:作用点主线的第一个时刻,call 时。ValidateRequestHeader 在 call 里决定放行还是短路(短路的枚举见四件套);AddAuthorization 在 call 里给请求加头,是客户端的层;AsyncRequireAuthorization 把判断推迟到响应 future 里,内层的 call 也随之推迟。

flowchart TB
  r[请求] --> v{"ValidateRequest::validate(&mut req)"}
  v -->|"Ok(())"| inner["调内层"]
  v -->|"Err(response)"| short["Kind::Error:直接返回 401 / 403 / 406"]
  r2[请求] --> a["AsyncRequireAuthorization::call:换出已就绪的内层"]
  a --> fut["future 里:authorize(req).await"]
  fut -->|"Ok(req)"| later["这时才 inner.call(req)"]
  fut -->|"Err(response)"| rej["直接返回该响应"]

ValidateRequest:一个同步的闸门

ValidateRequest 只有一个方法:

pub trait ValidateRequest<B> {
    type ResponseBody;

    fn validate(&mut self, request: &mut Request<B>) -> Result<(), Response<Self::ResponseBody>>;
}

两点值得注意:拿到的是 &mut Request,校验时可以顺手改请求(比如解析完 token,把用户信息放进扩展);拒绝时由校验器自己造整个响应,状态码、头、body 都由它定。闭包 FnMut(&mut Request<B>) -> Result<(), Response<ResBody>> 直接实现了这个 trait,ValidateRequestHeaderLayer::custom(闭包) 就是最通用的用法。

ValidateRequestHeader::call 把结果分到 Kind::Future 或 Kind::Error 两个变体。名字里有 Header,其实校验器能看整个请求(除了 body 的内容,它是同步的,读不了 body)。

现成的校验器有两个,都返回空 body,所以都要求 ResBody: Default。它们的类型参数是响应 body 类型(AcceptHeader<ResBody> 里放一个 PhantomData<fn() -> ResBody>),编译器从内层服务推不出来时,要写成 ValidateRequestHeaderLayer::<AcceptHeader<Full<Bytes>>>::accept(...)。

AcceptHeader:406

AcceptHeader::validate:

        if !req.headers().contains_key(header::ACCEPT) {
            return Ok(());
        }
        // ...
                            match (mim.type_(), mim.subtype()) {
                                (t, s) if t == typ && s == subtype => true,
                                (t, mime::STAR) if t == typ => true,
                                (mime::STAR, mime::STAR) => true,
                                _ => false,
                            }
  • 没有 Accept 头:放行。
  • 把每个 Accept 值按 MimeIter 拆开,任何一项能接受配置的类型就放行;只比较 type / subtype,不看参数,包括 q。application/json;q=0 的意思是「不接受 JSON」,这里照样放行。
  • 配置的值在构造时 parse::<Mime>(),不合法就 panic(value is not a valid header value)。

has_header_value:403

0.7.0 加的 has_header_value(#360)要求某个头等于某个值,否则 403。头名在构造时解析,不合法返回 Err(InvalidHeaderName),不 panic。比较用的是 headers().get(...),只取第一个值,转成 &str 后精确比较,大小写敏感、不去空白。

它适合「网关会加一个固定头,后端只接受经过网关的请求」这种场景。当作 API key 用时要知道:== 比较字符串,耗时随第一个不同字节的位置变化,不是常数时间比较。

弃用的 bearer 与 basic

ValidateRequestHeaderLayer::bearer / basic 不在 validate_request.rs 里,而在 auth/require_authorization.rs,这个文件第一行是模块级的弃用:

#![deprecated(since = "0.6.7", note = "too basic to be useful in real applications")]

这两个方法是给别的模块里的类型(ValidateRequestHeaderLayer)加的 inherent 方法,但定义在被弃用的模块里,调用时照样报警告,下面的实验能看到原文。它们的实现很直白:构造时拼好 Bearer {token} 或 Basic {base64(user:pass)},校验时和请求的 Authorization 做 HeaderValue 的字节比较,不等就 401;Basic 额外带上 WWW-Authenticate: Basic,Bearer 不带。所以 scheme 大小写(bearer t)、多一个空格都会被拒,这和 RFC 9110「认证 scheme 不区分大小写」不一致,也是弃用说明里「too basic」的一个注脚。

历史上这里绕过一圈:最早有独立的 RequireAuthorization 中间件,0.3.5 加了 ValidateRequest(#289),0.4.0 删掉 RequireAuthorization、把 bearer / basic 改成 ValidateRequest 的两个实现(#290),0.6.7 再整体弃用(#591)。

AddAuthorization:客户端的层

AddAuthorization::call 只有一句 req.headers_mut().insert(AUTHORIZATION, self.value.clone()):

  • 用的是 insert,调用方自己写的 Authorization 会被覆盖。
  • 值默认不标记敏感。要藏住它,构造时调 as_sensitive(true)。标记对 Debug 和 HTTP/2 的 HPACK 有什么效果,见搬运请求标识与头。AddAuthorizationLayer 自己派生了 Debug,不标记时打印 layer 也会把 token 打出来。
  • 它在 FollowRedirect 里面时,每一跳都会重新加上,FilterCredentials 去不掉;在外面时只加给第一跳,之后才由重定向策略决定去留。这个顺序问题放在拼一个客户端栈。

AsyncRequireAuthorization:鉴权期间占着内层

异步鉴权(查数据库、调鉴权服务)不能在 call 里同步做完。AsyncRequireAuthorization::call 只是启动 authorize,把已经就绪的那个内层服务换出来交给 future:

    fn call(&mut self, req: Request<ReqBody>) -> Self::Future {
        let mut inner = self.inner.clone();
        let authorize = self.auth.authorize(req);
        // mem::swap due to https://docs.rs/tower/latest/tower/trait.Service.html#be-careful-when-cloning-inner-services
        mem::swap(&mut self.inner, &mut inner);

future 里先等鉴权,Ok(req) 才 this.service.call(req),Err(res) 直接返回那个响应。拒绝响应由 AsyncAuthorizeRequest 自己给,所以这里不要求 body 实现 Default;Ok 时还可以换掉请求 body 的类型(关联类型 RequestBody)。

这和 tower 的 AsyncFilter 是同一个形状,为什么要克隆、为什么必须交出就绪的那一份,tower 卷 Filter 讲过;0.6.4 之前这里交出去的是新克隆的那份(#561 修复,CHANGELOG:「use the original inner service after it is ready, instead of using a clone」),没就绪的副本被 call,正是 tower 卷 poll_ready must be called first 那一类问题。

修好之后还有一个容易忽略的后果:poll_ready 预留的东西(并发许可、Buffer 的通道位置)在整个鉴权期间都被这个 future 占着,鉴权多慢,内层的名额就被占多久,哪怕请求最后被拒。下面的实验里内层是 ConcurrencyLimit(1),鉴权要 100 毫秒。

实验

tower-http = { version = "=0.7.1", features = ["validate-request", "auth"] }
tower = { version = "=0.5.3", features = ["util", "limit"] }
tokio = { version = "=1.54.1", features = ["macros", "rt", "time", "test-util"] }
http = "1"
http-body-util = "0.1"
bytes = "1"
use bytes::Bytes;
use http::{header, Request, Response, StatusCode};
use http_body_util::Full;
use std::{convert::Infallible, time::Duration};
use tokio::time::Instant;
use tower::{limit::ConcurrencyLimitLayer, ServiceBuilder, ServiceExt};
use tower_http::{
  auth::{AddAuthorizationLayer, AsyncRequireAuthorizationLayer},
  validate_request::ValidateRequestHeaderLayer,
};

type B = Full<Bytes>;

async fn ok(_: Request<B>) -> Result<Response<B>, Infallible> {
  Ok(Response::new(B::default()))
}

async fn send<S>(svc: &S, headers: &[(&str, &str)]) -> StatusCode
where
  S: tower::Service<Request<B>, Response = Response<B>, Error = Infallible> + Clone,
{
  let mut r = Request::get("/");
  for (k, v) in headers {
    r = r.header(*k, *v);
  }
  svc.clone().oneshot(r.body(B::default()).unwrap()).await.unwrap().status()
}

#[tokio::main(flavor = "current_thread", start_paused = true)]
async fn main() {
  println!("ValidateRequestHeaderLayer::accept(\"application/json\")");
  let svc = ServiceBuilder::new().layer(ValidateRequestHeaderLayer::accept("application/json")).service_fn(ok);
  for accept in [None, Some("application/json"), Some("application/*"), Some("*/*"), Some("text/html"), Some("application/json;q=0"), Some("text/html, application/json")] {
    let h: Vec<_> = accept.iter().map(|v| ("accept", *v)).collect();
    println!("  Accept: {:<28} -> {}", format!("{accept:?}"), send(&svc, &h).await);
  }

  println!("ValidateRequestHeaderLayer::has_header_value(\"x-key\", \"k1\")");
  let svc = ServiceBuilder::new().layer(ValidateRequestHeaderLayer::has_header_value("x-key", "k1").unwrap()).service_fn(ok);
  for h in [vec![], vec![("x-key", "k1")], vec![("x-key", "K1")], vec![("x-key", "k1"), ("x-key", "k2")], vec![("x-key", "k2"), ("x-key", "k1")]] {
    println!("  {:<40} -> {}", format!("{h:?}"), send(&svc, &h).await);
  }

  println!("deprecated bearer(\"t\")");
  #[allow(deprecated)]
  let svc = ServiceBuilder::new().layer(ValidateRequestHeaderLayer::bearer("t")).service_fn(ok);
  for v in ["Bearer t", "bearer t", "Bearer  t"] {
    println!("  Authorization: {v:<10} -> {}", send(&svc, &[("authorization", v)]).await);
  }

  println!("AddAuthorizationLayer on the client side");
  for sensitive in [false, true] {
    let svc = ServiceBuilder::new()
      .layer(AddAuthorizationLayer::bearer("s3cr3t").as_sensitive(sensitive))
      .service_fn(|req: Request<B>| async move {
        println!("    as_sensitive({sensitive}): inner sees {:?}", req.headers().get_all(header::AUTHORIZATION).iter().collect::<Vec<_>>());
        Ok::<_, Infallible>(Response::new(B::default()))
      });
    send(&svc, &[("authorization", "Bearer user-supplied")]).await;
  }

  println!("AsyncRequireAuthorization over ConcurrencyLimit(1), auth takes 100 ms");
  let start = Instant::now();
  let svc = ServiceBuilder::new()
    .layer(AsyncRequireAuthorizationLayer::new(move |req: Request<B>| async move {
      let who = req.uri().path().to_owned();
      println!("    {:>3} ms authorize {who} start", start.elapsed().as_millis());
      tokio::time::sleep(Duration::from_millis(100)).await;
      println!("    {:>3} ms authorize {who} done", start.elapsed().as_millis());
      Ok::<_, Response<B>>(req)
    }))
    .layer(ConcurrencyLimitLayer::new(1))
    .service_fn(move |req: Request<B>| async move {
      println!("    {:>3} ms handler {}", start.elapsed().as_millis(), req.uri().path());
      tokio::time::sleep(Duration::from_millis(10)).await;
      Ok::<_, Infallible>(Response::new(B::default()))
    });
  let a = svc.clone().oneshot(Request::get("/a").body(B::default()).unwrap());
  let b = svc.clone().oneshot(Request::get("/b").body(B::default()).unwrap());
  let _ = tokio::join!(a, b);
  println!("    {:>3} ms both done", start.elapsed().as_millis());
}

输出(M2 Max、rustc 1.99.0,暂停时钟,毫秒数是 tokio 的虚拟时间,跑 3 次相同):

ValidateRequestHeaderLayer::accept("application/json")
  Accept: None                         -> 200 OK
  Accept: Some("application/json")     -> 200 OK
  Accept: Some("application/*")        -> 200 OK
  Accept: Some("*/*")                  -> 200 OK
  Accept: Some("text/html")            -> 406 Not Acceptable
  Accept: Some("application/json;q=0") -> 200 OK
  Accept: Some("text/html, application/json") -> 200 OK
ValidateRequestHeaderLayer::has_header_value("x-key", "k1")
  []                                       -> 403 Forbidden
  [("x-key", "k1")]                        -> 200 OK
  [("x-key", "K1")]                        -> 403 Forbidden
  [("x-key", "k1"), ("x-key", "k2")]       -> 200 OK
  [("x-key", "k2"), ("x-key", "k1")]       -> 403 Forbidden
deprecated bearer("t")
  Authorization: Bearer t   -> 200 OK
  Authorization: bearer t   -> 401 Unauthorized
  Authorization: Bearer  t  -> 401 Unauthorized
AddAuthorizationLayer on the client side
    as_sensitive(false): inner sees ["Bearer s3cr3t"]
    as_sensitive(true): inner sees [Sensitive]
AsyncRequireAuthorization over ConcurrencyLimit(1), auth takes 100 ms
      0 ms authorize /a start
    100 ms authorize /a done
    100 ms handler /a
    110 ms authorize /b start
    210 ms authorize /b done
    210 ms handler /b
    220 ms both done

去掉 #[allow(deprecated)] 时 rustc 1.99.0 的警告:

warning: use of deprecated associated function `tower_http::auth::require_authorization::<impl tower_http::validate_request::ValidateRequestHeaderLayer<tower_http::auth::require_authorization::Bearer<ResBody>>>::bearer`: too basic to be useful in real applications
  • Accept:q=0 照样 200;两个类型里有一个匹配就放行。
  • has_header_value:同一个头出现两次,只看第一个,k2, k1 的顺序被拒。
  • bearer:小写 scheme、两个空格都是 401。
  • AddAuthorization:调用方写的 Bearer user-supplied 被覆盖,内层只看到一个值;标记后 Debug 成了 Sensitive。
  • AsyncRequireAuthorization:/b 的鉴权直到 110 毫秒才开始。/b 的 oneshot 先要 poll_ready,而唯一的并发许可在 0 毫秒就被 /a 拿走,跟着就绪的内层被换进了 /a 的 future,鉴权、handler 一路占到 110 毫秒。两个请求的鉴权本可以并行,结果被内层的并发上限串了起来。把 ConcurrencyLimit 挪到鉴权层外面,限的就是「鉴权 + 处理」整体;只想限 handler 的话,得让鉴权层不持有内层的就绪状态,AsyncRequireAuthorization 的结构做不到这一点。

小结

  • ValidateRequest::validate(&mut req) 同步放行或给出完整的拒绝响应,可以顺手改请求;闭包直接实现它。现成的两个校验器回空 body,要求 ResBody: Default。
  • AcceptHeader:没有 Accept 放行,不看参数和 q 值,非法配置在构造时 panic,拒绝回 406。has_header_value(0.7.0,#360):只看第一个值,大小写敏感,非常数时间比较,拒绝回 403。
  • bearer / basic 定义在 0.6.7 起整体弃用的 require_authorization 模块里,调用就报警告;字节比较,scheme 大小写敏感,Basic 才带 WWW-Authenticate。
  • AddAuthorization 是客户端的:insert 覆盖已有值,默认不标敏感(as_sensitive(true)),和 FollowRedirect 的相对位置决定凭据能不能被过滤。
  • AsyncRequireAuthorization 用 mem::swap 把就绪的内层交给 future(0.6.4,#561);后果是内层的就绪名额在整个鉴权期间被占着,慢鉴权会被内层的并发上限串行化。

💬 关联讨论

发起讨论
加载中...