From bcaeb0beefe2442db130b69a118a8c4f8b323185 Mon Sep 17 00:00:00 2001 From: iven Date: Mon, 27 Apr 2026 18:41:28 +0800 Subject: [PATCH] =?UTF-8?q?feat(server):=20tenant=20RLS=20=E4=B8=AD?= =?UTF-8?q?=E9=97=B4=E4=BB=B6=20=E2=80=94=20SET=20app.current=5Ftenant=5Fi?= =?UTF-8?q?d?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 新增 tenant_rls_middleware:JWT 解析后 SET 租户 ID,请求结束 RESET - 挂载到 protected router 的 JWT 层之后 - SET 失败仅 warn 不阻断(RLS 是安全网,主隔离在应用层) - RESET 防止连接池复用时租户上下文泄漏 --- crates/erp-server/src/main.rs | 8 +++ crates/erp-server/src/middleware/mod.rs | 1 + .../erp-server/src/middleware/tenant_rls.rs | 49 +++++++++++++++++++ 3 files changed, 58 insertions(+) create mode 100644 crates/erp-server/src/middleware/tenant_rls.rs diff --git a/crates/erp-server/src/main.rs b/crates/erp-server/src/main.rs index a10d408..198c0a7 100644 --- a/crates/erp-server/src/main.rs +++ b/crates/erp-server/src/main.rs @@ -556,6 +556,14 @@ async fn main() -> anyhow::Result<()> { async move { jwt_auth_middleware_fn(secret, Some(db), req, next).await } }) }) + // Tenant RLS — 在 JWT 之后执行,SET app.current_tenant_id + .layer({ + let db = state.db.clone(); + axum_middleware::from_fn(move |req, next| { + let db = db.clone(); + async move { middleware::tenant_rls::tenant_rls_middleware(db, req, next).await } + }) + }) .with_state(state.clone()); // Merge public + protected into the final application router diff --git a/crates/erp-server/src/middleware/mod.rs b/crates/erp-server/src/middleware/mod.rs index 382585d..9b7d07e 100644 --- a/crates/erp-server/src/middleware/mod.rs +++ b/crates/erp-server/src/middleware/mod.rs @@ -1 +1,2 @@ pub mod rate_limit; +pub mod tenant_rls; diff --git a/crates/erp-server/src/middleware/tenant_rls.rs b/crates/erp-server/src/middleware/tenant_rls.rs new file mode 100644 index 0000000..318cd75 --- /dev/null +++ b/crates/erp-server/src/middleware/tenant_rls.rs @@ -0,0 +1,49 @@ +use axum::body::Body; +use axum::http::Request; +use axum::middleware::Next; +use axum::response::Response; +use erp_core::types::TenantContext; +use sea_orm::ConnectionTrait; + +/// Tenant RLS 中间件。 +/// +/// 从 request extensions 中提取 `TenantContext`,在数据库连接上设置 +/// `app.current_tenant_id`,使 PostgreSQL RLS 策略自动按租户过滤。 +/// +/// 请求处理完成后自动 RESET 设置,防止连接池复用时泄漏。 +/// +/// SET 失败时仅 warn 不阻断请求(RLS 是安全网,主隔离仍在应用层)。 +pub async fn tenant_rls_middleware( + db: sea_orm::DatabaseConnection, + req: Request, + next: Next, +) -> Response { + let tenant_id = req.extensions().get::().map(|ctx| ctx.tenant_id); + + if let Some(tid) = tenant_id { + // SET app.current_tenant_id — RLS 策略读取此值 + if let Err(e) = db + .execute_unprepared(&format!( + "SET app.current_tenant_id = '{}'", + tid + )) + .await + { + tracing::warn!(tenant_id = %tid, error = %e, "SET app.current_tenant_id 失败(RLS 未激活)"); + } + } + + let response = next.run(req).await; + + // RESET — 防止连接池复用时泄漏租户上下文 + if tenant_id.is_some() { + if let Err(e) = db + .execute_unprepared("RESET app.current_tenant_id") + .await + { + tracing::debug!(error = %e, "RESET app.current_tenant_id 失败(非致命)"); + } + } + + response +}