日志里还是有 authorization
主线位置:作用点主线的前两个时刻。
Trace在call时打印请求头,在响应 future 完成时打印响应头;敏感标记必须在它打印之前打上。标记本身只是HeaderValue::set_sensitive(true),原理和顺序规则在 搬运请求标识与头 的最后一节;这一页把四种常见摆法跑出真实的日志行。
sequenceDiagram participant O as 外层标记 participant T as Trace participant I as 内层标记 participant H as handler O->>T: call:请求头已标记 T->>T: make_span 打印请求头 T->>I: call I->>H: call(这里才标记请求头,太晚) H-->>I: 响应带 set-cookie I-->>T: 响应头已标记 T->>T: on_response 打印响应头 T-->>O: 响应(这里才标记响应头,太晚)
复现
handler 回一个 set-cookie,请求带 Authorization: Bearer s3cr3t。Trace 打开两处 include_headers:请求头进 span 字段(make_span.rs 第 91 行),响应头进 on_response 事件(on_response.rs 第 110 行),两处都用 Debug 打印 HeaderMap。
tower-http = { version = "=0.7.1", features = ["trace", "sensitive-headers"] }
tower = { version = "=0.5.3", features = ["util"] }
tokio = { version = "=1.54.1", features = ["macros", "rt"] }
http = "1"
http-body-util = "0.1"
bytes = "1"
tracing = "0.1"
tracing-subscriber = { version = "0.3", features = ["fmt"] }
use bytes::Bytes;
use http::{header, Request, Response};
use http_body_util::{BodyExt, Full};
use std::convert::Infallible;
use tower::{Service, ServiceBuilder, ServiceExt};
use tower_http::sensitive_headers::{SetSensitiveHeadersLayer, SetSensitiveRequestHeadersLayer, SetSensitiveResponseHeadersLayer};
use tower_http::trace::{DefaultMakeSpan, DefaultOnResponse, TraceLayer};
use tracing::Level;
async fn handler(_: Request<Full<Bytes>>) -> Result<Response<Full<Bytes>>, Infallible> {
let mut res = Response::new(Full::from("ok"));
res.headers_mut().insert(header::SET_COOKIE, "sid=abc123".parse().unwrap());
Ok(res)
}
fn trace() -> TraceLayer<tower_http::classify::SharedClassifier<tower_http::classify::ServerErrorsAsFailures>, DefaultMakeSpan, tower_http::trace::DefaultOnRequest, DefaultOnResponse> {
TraceLayer::new_for_http()
.make_span_with(DefaultMakeSpan::new().include_headers(true))
.on_response(DefaultOnResponse::new().level(Level::INFO).include_headers(true))
}
async fn run<S>(name: &str, svc: S)
where
S: Service<Request<Full<Bytes>>, Response = Response<tower_http::trace::ResponseBody<Full<Bytes>, tower_http::classify::NeverClassifyEos<tower_http::classify::ServerErrorsFailureClass>>>, Error = Infallible>,
{
println!("--- {name}");
let req = Request::get("/").header(header::AUTHORIZATION, "Bearer s3cr3t").body(Full::default()).unwrap();
let res = svc.oneshot(req).await.unwrap();
let _ = res.into_body().collect().await;
}
#[tokio::main(flavor = "current_thread")]
async fn main() {
tracing_subscriber::fmt().with_max_level(Level::DEBUG).with_ansi(false).without_time().with_target(false).init();
let auth = [header::AUTHORIZATION];
let cookie = [header::SET_COOKIE];
run("no marking", ServiceBuilder::new().layer(trace()).service_fn(handler)).await;
run(
"request marker inside Trace",
ServiceBuilder::new().layer(trace()).layer(SetSensitiveRequestHeadersLayer::new(auth.clone())).service_fn(handler),
)
.await;
run(
"SetSensitiveHeadersLayer outside Trace",
ServiceBuilder::new().layer(SetSensitiveHeadersLayer::new([header::AUTHORIZATION, header::SET_COOKIE])).layer(trace()).service_fn(handler),
)
.await;
run(
"request outside, response inside",
ServiceBuilder::new()
.layer(SetSensitiveRequestHeadersLayer::new(auth))
.layer(trace())
.layer(SetSensitiveResponseHeadersLayer::new(cookie))
.service_fn(handler),
)
.await;
}
M2 Max、rustc 1.99.0,跑 3 次输出相同(每组第三行 end of stream 和第一行的 span 相同,省略):
--- no marking
DEBUG request{method=GET uri=/ version=HTTP/1.1 headers={"authorization": "Bearer s3cr3t"}}: started processing request
INFO request{method=GET uri=/ version=HTTP/1.1 headers={"authorization": "Bearer s3cr3t"}}: finished processing request latency=0 ms status=200 response_headers={"set-cookie": "sid=abc123"}
--- request marker inside Trace
DEBUG request{method=GET uri=/ version=HTTP/1.1 headers={"authorization": "Bearer s3cr3t"}}: started processing request
INFO request{method=GET uri=/ version=HTTP/1.1 headers={"authorization": "Bearer s3cr3t"}}: finished processing request latency=0 ms status=200 response_headers={"set-cookie": "sid=abc123"}
--- SetSensitiveHeadersLayer outside Trace
DEBUG request{method=GET uri=/ version=HTTP/1.1 headers={"authorization": Sensitive}}: started processing request
INFO request{method=GET uri=/ version=HTTP/1.1 headers={"authorization": Sensitive}}: finished processing request latency=0 ms status=200 response_headers={"set-cookie": "sid=abc123"}
--- request outside, response inside
DEBUG request{method=GET uri=/ version=HTTP/1.1 headers={"authorization": Sensitive}}: started processing request
INFO request{method=GET uri=/ version=HTTP/1.1 headers={"authorization": Sensitive}}: finished processing request latency=0 ms status=200 response_headers={"set-cookie": Sensitive}
- 标记层在
Trace里面:和没加一样。Trace::call先建 span、把请求头写进字段,然后才调内层;标记层的call轮到时,span 里的字符串早就生成了。请求头写进的是 span 字段,之后这个请求的每一行日志都带着它,泄露一次就是每行一次。 - 两侧合一的
SetSensitiveHeadersLayer放在外面:请求头藏住了,set-cookie没藏住。SetSensitiveHeaders<S>只是SetSensitiveRequestHeaders<SetSensitiveResponseHeaders<S>>的别名,两个标记层贴在一起,整体放在Trace外面时,响应侧的标记在Trace打印之后才发生。放在里面则反过来,请求头漏。 - 请求侧在外、响应侧在里:两边都是
Sensitive。这就是模块文档示例的摆法。
修法
ServiceBuilder::new()
.layer(SetSensitiveRequestHeadersLayer::new([header::AUTHORIZATION, header::COOKIE]))
.layer(TraceLayer::new_for_http().make_span_with(DefaultMakeSpan::new().include_headers(true)))
.layer(SetSensitiveResponseHeadersLayer::new([header::SET_COOKIE]))
在 axum 里 .layer 的顺序和 ServiceBuilder 相反,后写的在外:先写响应侧、再写 TraceLayer、最后写请求侧。
两个附带的检查:
- 标记只影响
Debug(和 HTTP/2 的 HPACK 索引),Display不存在、to_str()照样拿到原值。自己写的on_request闭包里用to_str()拼日志,标记不起作用。 - 不需要看头的话,最省事的是不开
include_headers,默认就是关的。
小结
Trace在call时打印请求头、在响应 future 完成时打印响应头,标记要在它打印之前完成。- 请求侧标记层放在
Trace外面,响应侧放在里面;合一的SetSensitiveHeadersLayer只是两层的别名,放哪边都会漏一侧。 - 标记只改
Debug输出,自己用to_str()打出来的照样是明文。
💬 关联讨论
发起讨论加载中...