校验与鉴权
主线位置:作用点主线的第一个时刻,
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
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);后果是内层的就绪名额在整个鉴权期间被占着,慢鉴权会被内层的并发上限串行化。