feat(server): tenant RLS 中间件 — SET app.current_tenant_id
- 新增 tenant_rls_middleware:JWT 解析后 SET 租户 ID,请求结束 RESET - 挂载到 protected router 的 JWT 层之后 - SET 失败仅 warn 不阻断(RLS 是安全网,主隔离在应用层) - RESET 防止连接池复用时租户上下文泄漏
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -1 +1,2 @@
|
||||
pub mod rate_limit;
|
||||
pub mod tenant_rls;
|
||||
|
||||
49
crates/erp-server/src/middleware/tenant_rls.rs
Normal file
49
crates/erp-server/src/middleware/tenant_rls.rs
Normal file
@@ -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<Body>,
|
||||
next: Next,
|
||||
) -> Response {
|
||||
let tenant_id = req.extensions().get::<TenantContext>().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
|
||||
}
|
||||
Reference in New Issue
Block a user