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:
iven
2026-04-27 18:41:28 +08:00
parent b7b9f50d00
commit bcaeb0beef
3 changed files with 58 additions and 0 deletions

View File

@@ -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

View File

@@ -1 +1,2 @@
pub mod rate_limit;
pub mod tenant_rls;

View 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
}