Rocket Fairings 实战把全局能力做成“结构化中间件”

1、一个最常用的 Fairing:RequestId + 耗时 + 访问日志

目标效果:

  • 每个请求生成一个唯一 RequestId(一次请求内只生成一次)
  • 输出访问日志:方法、路径、状态码、耗时、request_id
  • 在响应头里注入 X-Request-IdX-Response-Time-ms

代码(直接可用):

rust 复制代码
#[macro_use] extern crate rocket;

use rocket::{Request, Data, Response};
use rocket::fairing::{Fairing, Info, Kind};
use rocket::http::Header;
use std::sync::atomic::{AtomicUsize, Ordering};
use std::time::Instant;

static REQ_SEQ: AtomicUsize = AtomicUsize::new(1);

struct ReqMeta {
    id: usize,
    start: Instant,
}

struct TraceAndLog;

#[rocket::async_trait]
impl Fairing for TraceAndLog {
    fn info(&self) -> Info {
        Info { name: "Trace + AccessLog", kind: Kind::Request | Kind::Response }
    }

    async fn on_request(&self, req: &mut Request<'_>, _: &mut Data<'_>) {
        // request-local state:每个请求只生成一次
        let meta = req.local_cache(|| ReqMeta {
            id: REQ_SEQ.fetch_add(1, Ordering::Relaxed),
            start: Instant::now(),
        });

        // 这里可以只打"入站日志",也可以不打,留到 on_response 打完整日志
        let ip = req.client_ip().map(|x| x.to_string()).unwrap_or_else(|| "-".into());
        println!("[IN ] id={} ip={} {} {}", meta.id, ip, req.method(), req.uri());
    }

    async fn on_response<'r>(&self, req: &'r Request<'_>, res: &mut Response<'r>) {
        let meta = req.local_cache(|| ReqMeta {
            id: REQ_SEQ.fetch_add(1, Ordering::Relaxed),
            start: Instant::now(),
        });

        let elapsed_ms = meta.start.elapsed().as_millis();

        res.set_header(Header::new("X-Request-Id", meta.id.to_string()));
        res.set_header(Header::new("X-Response-Time-ms", elapsed_ms.to_string()));

        println!(
            "[OUT] id={} {} {} -> {} ({}ms)",
            meta.id,
            req.method(),
            req.uri(),
            res.status(),
            elapsed_ms
        );
    }
}

#[get("/")]
fn index() -> &'static str {
    "ok"
}

#[launch]
fn rocket() -> _ {
    rocket::build()
        .attach(TraceAndLog)
        .mount("/", routes![index])
}

要点:

  • local_cache 是关键:它让你在同一个请求生命周期里,多次取到同一个对象(避免重复生成 id/重复计时)。
  • Fairing 的执行顺序按 attach 顺序,多个 fairing 叠加时要注意"谁先注入,谁后覆盖"。

2、统一安全响应头:全站加固的"最小集合"

典型要加的头(按需选):

  • X-Content-Type-Options: nosniff
  • X-Frame-Options: DENYSAMEORIGIN
  • Referrer-Policy
  • Content-Security-Policy(CSP 要结合前端资源策略)
  • Strict-Transport-Security(HSTS 只建议在 HTTPS 场景开启)

用 response fairing 一次性注入:

rust 复制代码
use rocket::{Request, Response};
use rocket::fairing::{Fairing, Info, Kind};
use rocket::http::Header;

struct SecurityHeaders;

#[rocket::async_trait]
impl Fairing for SecurityHeaders {
    fn info(&self) -> Info {
        Info { name: "Security Headers", kind: Kind::Response }
    }

    async fn on_response<'r>(&self, _req: &'r Request<'_>, res: &mut Response<'r>) {
        res.set_header(Header::new("X-Content-Type-Options", "nosniff"));
        res.set_header(Header::new("X-Frame-Options", "DENY"));
        res.set_header(Header::new("Referrer-Policy", "no-referrer"));

        // CSP 示例:实际要按你站点资源加载策略调整
        res.set_header(Header::new(
            "Content-Security-Policy",
            "default-src 'self'; object-src 'none'; frame-ancestors 'none'; base-uri 'self'"
        ));

        // HSTS:只有你的站点稳定跑在 HTTPS 上才建议开启
        // res.set_header(Header::new("Strict-Transport-Security", "max-age=31536000; includeSubDomains"));
    }
}

3、CORS:Fairing 负责"注入头",OPTIONS 由路由负责"响应"

CORS 的坑点:浏览器的预检请求是 OPTIONS,如果你只在 Fairing 里加头但没有对应 OPTIONS 路由,还是会 404。

做法:

  • Response Fairing:对所有响应加 CORS 头
  • 一个兜底 OPTIONS 路由:专门处理预检
rust 复制代码
#[macro_use] extern crate rocket;

use rocket::{Request, Response};
use rocket::fairing::{Fairing, Info, Kind};
use rocket::http::{Header, Status};

struct Cors;

#[rocket::async_trait]
impl Fairing for Cors {
    fn info(&self) -> Info {
        Info { name: "CORS", kind: Kind::Response }
    }

    async fn on_response<'r>(&self, _req: &'r Request<'_>, res: &mut Response<'r>) {
        res.set_header(Header::new("Access-Control-Allow-Origin", "*"));
        res.set_header(Header::new("Access-Control-Allow-Methods", "GET,POST,PUT,DELETE,OPTIONS"));
        res.set_header(Header::new("Access-Control-Allow-Headers", "Content-Type,Authorization"));
        res.set_header(Header::new("Access-Control-Max-Age", "86400"));
    }
}

// 兜底预检:让所有 OPTIONS 都返回 204
#[options("/<_..>")]
fn all_options() -> Status {
    Status::NoContent
}

#[get("/ping")]
fn ping() -> &'static str { "pong" }

#[launch]
fn rocket() -> _ {
    rocket::build()
        .attach(Cors)
        .mount("/", routes![ping, all_options])
}

4、404 重写:给 SPA(前端路由)一个"始终返回 index.html"的后门

很多前端单页应用(React/Vue)刷新 /app/settings 会命中后端 404。你想要的其实是:后端把 404 重写成 index.html,让前端路由接管。

这类逻辑非常适合写在 response fairing 里:

rust 复制代码
use rocket::{Request, Response};
use rocket::fairing::{Fairing, Info, Kind};
use rocket::http::{Status, ContentType};
use std::io::Cursor;

struct SpaFallback;

#[rocket::async_trait]
impl Fairing for SpaFallback {
    fn info(&self) -> Info {
        Info { name: "SPA 404 Fallback", kind: Kind::Response }
    }

    async fn on_response<'r>(&self, req: &'r Request<'_>, res: &mut Response<'r>) {
        if res.status() != Status::NotFound {
            return;
        }

        // 只对 GET 做 fallback,避免把 API 的 404 也吞掉
        if req.method() != rocket::http::Method::Get {
            return;
        }

        // 简单规则:/api 开头不重写(你可按需调整)
        if req.uri().path().starts_with("/api") {
            return;
        }

        let index_html = r#"<!doctype html><html><body><div id="app"></div></body></html>"#;

        res.set_status(Status::Ok);
        res.set_header(ContentType::HTML);
        res.set_sized_body(index_html.len(), Cursor::new(index_html));
    }
}

生产建议:index_html 真实项目里不要硬编码,应该用 NamedFile 或构建产物目录读取。

5、启动阶段 on_ignite:校验配置 + 注入 Managed State

Ignite 阶段是"应用还没真正启动监听端口"的时候,你可以:

  • 从配置里取值
  • 做合法性校验,不合法就阻止启动
  • 把解析后的结果塞进 manage() 的状态里供业务使用

用 AdHoc 写法最省事:

rust 复制代码
use rocket::fairing::AdHoc;

#[derive(Clone)]
struct MyConfig {
    service_name: String,
}

fn config_fairing() -> AdHoc {
    AdHoc::on_ignite("Config Validator", |rocket| Box::pin(async move {
        // 这里演示从环境变量读取,实际也可以用 figment extract
        let service_name = std::env::var("SERVICE_NAME").unwrap_or_else(|_| "rocket-app".into());

        if service_name.trim().is_empty() {
            panic!("SERVICE_NAME is empty, abort launch");
        }

        rocket.manage(MyConfig { service_name })
    }))
}

挂载:

rust 复制代码
rocket::build().attach(config_fairing())

这种写法很适合"必须要有某个配置才能启动"的场景:数据库 DSN、外部依赖地址、加密密钥等。

6、liftoff / shutdown:启动伴生任务与优雅收尾

你经常会在服务启动后做一些事:

  • 打印启动信息
  • 启动一个后台协程(例如定时刷新缓存、订阅消息)
  • 停机时通知外部系统、flush 日志等

AdHoc 版:

rust 复制代码
use rocket::fairing::AdHoc;

fn liftoff_and_shutdown() -> AdHoc {
    AdHoc::on_liftoff("Liftoff Hook", |_| Box::pin(async move {
        println!("Rocket launched, start background workers...");
    }))
}

再加一个 shutdown:

rust 复制代码
fn shutdown_hook() -> AdHoc {
    AdHoc::on_shutdown("Shutdown Hook", |_| Box::pin(async move {
        println!("Shutdown started, do cleanup...");
    }))
}

注意:shutdown fairings 会并发执行,Rocket 会等待它们完成再继续关停流程。

7、别用 Fairing 做鉴权:给你一个"正确的混合姿势"

你可能想:能不能在 on_request 里检查 token,没有就拒绝?

不行,Fairing 不能直接响应或终止请求。

正确姿势是:

  • Fairing:注入 request_id、打点、统一头
  • Guard:做鉴权/授权并决定 forward/deny
  • Handler:只写业务

这样你的鉴权还能做到"只对某些路由生效",而不是全站硬塞。、

相关推荐
Andrew_Ryan12 小时前
rust arena 内存分配
rust
Andrew_Ryan12 小时前
深入理解 Rust 内存管理:基于 typed_arena 的指针操作实践
rust
岁岁种桃花儿1 天前
Kafka从入门到上天系列第一篇:kafka的安装和启动
大数据·中间件·kafka
微小冷2 天前
Rust异步编程详解
开发语言·rust·async·await·异步编程·tokio
鸿乃江边鸟2 天前
Spark Datafusion Comet 向量化Rust Native--CometShuffleExchangeExec怎么控制读写
大数据·rust·spark·native
明飞19872 天前
tauri
rust
咚为2 天前
Rust tokio:Task ≠ Thread:Tokio 调度模型中的“假并发”与真实代价
开发语言·后端·rust
波波0073 天前
每日一题:中间件是如何工作的?
中间件·.net·面试题
玄同7653 天前
LangChain 1.0 框架全面解析:从架构到实践
人工智能·深度学习·自然语言处理·中间件·架构·langchain·rag