From 080d2cb3d6dd331b9432306792444fcd7158a64d Mon Sep 17 00:00:00 2001 From: iven Date: Fri, 17 Apr 2026 17:45:59 +0800 Subject: [PATCH] =?UTF-8?q?fix(security):=20Q2=20Chunk=202=20=E2=80=94=20?= =?UTF-8?q?=E5=A4=9A=E7=A7=9F=E6=88=B7=E5=AE=89=E5=85=A8=E5=8A=A0=E5=9B=BA?= =?UTF-8?q?=20+=20=E9=99=90=E6=B5=81=20fail-closed?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - auth_service::refresh() 添加 tenant_id 校验 - user_service get_by_id/update/delete/assign_roles 改为数据库级 tenant_id 过滤 - 限流中间件改为 fail-closed:Redis 不可达时返回 429 而非放行 --- crates/erp-auth/src/service/auth_service.rs | 11 ++++++++ crates/erp-auth/src/service/user_service.rs | 24 ++++++++++++------ .../erp-server/src/middleware/rate_limit.rs | 25 ++++++++++++++----- 3 files changed, 46 insertions(+), 14 deletions(-) diff --git a/crates/erp-auth/src/service/auth_service.rs b/crates/erp-auth/src/service/auth_service.rs index 7909d3b..ae21d4c 100644 --- a/crates/erp-auth/src/service/auth_service.rs +++ b/crates/erp-auth/src/service/auth_service.rs @@ -180,6 +180,17 @@ impl AuthService { .map_err(|e| AuthError::Validation(e.to_string()))? .ok_or(AuthError::TokenRevoked)?; + // 验证用户属于 JWT 中声明的租户 + if user_model.tenant_id != claims.tid { + tracing::warn!( + user_id = %claims.sub, + jwt_tenant = %claims.tid, + actual_tenant = %user_model.tenant_id, + "Token tenant_id 与用户实际租户不匹配" + ); + return Err(AuthError::TokenRevoked); + } + let role_resps = Self::get_user_role_resps(claims.sub, claims.tid, db).await?; let user_resp = UserResp { id: user_model.id, diff --git a/crates/erp-auth/src/service/user_service.rs b/crates/erp-auth/src/service/user_service.rs index 630f505..29e9fa1 100644 --- a/crates/erp-auth/src/service/user_service.rs +++ b/crates/erp-auth/src/service/user_service.rs @@ -126,11 +126,13 @@ impl UserService { tenant_id: Uuid, db: &sea_orm::DatabaseConnection, ) -> AuthResult { - let user_model = user::Entity::find_by_id(id) + let user_model = user::Entity::find() + .filter(user::Column::Id.eq(id)) + .filter(user::Column::TenantId.eq(tenant_id)) + .filter(user::Column::DeletedAt.is_null()) .one(db) .await .map_err(|e| AuthError::Validation(e.to_string()))? - .filter(|u| u.tenant_id == tenant_id && u.deleted_at.is_none()) .ok_or_else(|| AuthError::Validation("用户不存在".to_string()))?; let roles = Self::fetch_user_role_resps(id, tenant_id, db).await?; @@ -193,11 +195,13 @@ impl UserService { req: &UpdateUserReq, db: &sea_orm::DatabaseConnection, ) -> AuthResult { - let user_model = user::Entity::find_by_id(id) + let user_model = user::Entity::find() + .filter(user::Column::Id.eq(id)) + .filter(user::Column::TenantId.eq(tenant_id)) + .filter(user::Column::DeletedAt.is_null()) .one(db) .await .map_err(|e| AuthError::Validation(e.to_string()))? - .filter(|u| u.tenant_id == tenant_id && u.deleted_at.is_none()) .ok_or_else(|| AuthError::Validation("用户不存在".to_string()))?; let next_ver = check_version(req.version, user_model.version) @@ -247,11 +251,13 @@ impl UserService { db: &sea_orm::DatabaseConnection, event_bus: &EventBus, ) -> AuthResult<()> { - let user_model = user::Entity::find_by_id(id) + let user_model = user::Entity::find() + .filter(user::Column::Id.eq(id)) + .filter(user::Column::TenantId.eq(tenant_id)) + .filter(user::Column::DeletedAt.is_null()) .one(db) .await .map_err(|e| AuthError::Validation(e.to_string()))? - .filter(|u| u.tenant_id == tenant_id && u.deleted_at.is_none()) .ok_or_else(|| AuthError::Validation("用户不存在".to_string()))?; let current_version = user_model.version; @@ -294,11 +300,13 @@ impl UserService { db: &sea_orm::DatabaseConnection, ) -> AuthResult> { // 验证用户存在 - let _user = user::Entity::find_by_id(user_id) + let _user = user::Entity::find() + .filter(user::Column::Id.eq(user_id)) + .filter(user::Column::TenantId.eq(tenant_id)) + .filter(user::Column::DeletedAt.is_null()) .one(db) .await .map_err(|e| AuthError::Validation(e.to_string()))? - .filter(|u| u.tenant_id == tenant_id && u.deleted_at.is_none()) .ok_or_else(|| AuthError::Validation("用户不存在".to_string()))?; // 验证所有角色存在且属于当前租户 diff --git a/crates/erp-server/src/middleware/rate_limit.rs b/crates/erp-server/src/middleware/rate_limit.rs index 3ca9fb2..f3708f8 100644 --- a/crates/erp-server/src/middleware/rate_limit.rs +++ b/crates/erp-server/src/middleware/rate_limit.rs @@ -118,9 +118,14 @@ async fn apply_rate_limit( ) -> Response { let avail = redis_avail(); - // 快速跳过:Redis 不可达时直接放行 + // Redis 不可达时 fail-closed:拒绝请求 if !avail.should_try().await { - return next.run(req).await; + tracing::warn!("Redis 不可达,启用 fail-closed 限流保护"); + let body = RateLimitResponse { + error: "Too Many Requests".to_string(), + message: "服务暂时不可用,请稍后重试".to_string(), + }; + return (StatusCode::TOO_MANY_REQUESTS, axum::Json(body)).into_response(); } let key = format!("rate_limit:{}:{}", prefix, identifier); @@ -131,17 +136,25 @@ async fn apply_rate_limit( c } Err(e) => { - tracing::warn!(error = %e, "Redis 连接失败,跳过限流"); + tracing::warn!(error = %e, "Redis 连接失败,fail-closed 限流保护"); avail.mark_failed().await; - return next.run(req).await; + let body = RateLimitResponse { + error: "Too Many Requests".to_string(), + message: "服务暂时不可用,请稍后重试".to_string(), + }; + return (StatusCode::TOO_MANY_REQUESTS, axum::Json(body)).into_response(); } }; let count: i64 = match redis::cmd("INCR").arg(&key).query_async(&mut conn).await { Ok(n) => n, Err(e) => { - tracing::warn!(error = %e, "Redis INCR 失败,跳过限流"); - return next.run(req).await; + tracing::warn!(error = %e, "Redis INCR 失败,fail-closed 限流保护"); + let body = RateLimitResponse { + error: "Too Many Requests".to_string(), + message: "服务暂时不可用,请稍后重试".to_string(), + }; + return (StatusCode::TOO_MANY_REQUESTS, axum::Json(body)).into_response(); } };