fix(security): Q2 Chunk 2 — 多租户安全加固 + 限流 fail-closed
- auth_service::refresh() 添加 tenant_id 校验 - user_service get_by_id/update/delete/assign_roles 改为数据库级 tenant_id 过滤 - 限流中间件改为 fail-closed:Redis 不可达时返回 429 而非放行
This commit is contained in:
@@ -180,6 +180,17 @@ impl AuthService {
|
|||||||
.map_err(|e| AuthError::Validation(e.to_string()))?
|
.map_err(|e| AuthError::Validation(e.to_string()))?
|
||||||
.ok_or(AuthError::TokenRevoked)?;
|
.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 role_resps = Self::get_user_role_resps(claims.sub, claims.tid, db).await?;
|
||||||
let user_resp = UserResp {
|
let user_resp = UserResp {
|
||||||
id: user_model.id,
|
id: user_model.id,
|
||||||
|
|||||||
@@ -126,11 +126,13 @@ impl UserService {
|
|||||||
tenant_id: Uuid,
|
tenant_id: Uuid,
|
||||||
db: &sea_orm::DatabaseConnection,
|
db: &sea_orm::DatabaseConnection,
|
||||||
) -> AuthResult<UserResp> {
|
) -> AuthResult<UserResp> {
|
||||||
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)
|
.one(db)
|
||||||
.await
|
.await
|
||||||
.map_err(|e| AuthError::Validation(e.to_string()))?
|
.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()))?;
|
.ok_or_else(|| AuthError::Validation("用户不存在".to_string()))?;
|
||||||
|
|
||||||
let roles = Self::fetch_user_role_resps(id, tenant_id, db).await?;
|
let roles = Self::fetch_user_role_resps(id, tenant_id, db).await?;
|
||||||
@@ -193,11 +195,13 @@ impl UserService {
|
|||||||
req: &UpdateUserReq,
|
req: &UpdateUserReq,
|
||||||
db: &sea_orm::DatabaseConnection,
|
db: &sea_orm::DatabaseConnection,
|
||||||
) -> AuthResult<UserResp> {
|
) -> AuthResult<UserResp> {
|
||||||
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)
|
.one(db)
|
||||||
.await
|
.await
|
||||||
.map_err(|e| AuthError::Validation(e.to_string()))?
|
.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()))?;
|
.ok_or_else(|| AuthError::Validation("用户不存在".to_string()))?;
|
||||||
|
|
||||||
let next_ver = check_version(req.version, user_model.version)
|
let next_ver = check_version(req.version, user_model.version)
|
||||||
@@ -247,11 +251,13 @@ impl UserService {
|
|||||||
db: &sea_orm::DatabaseConnection,
|
db: &sea_orm::DatabaseConnection,
|
||||||
event_bus: &EventBus,
|
event_bus: &EventBus,
|
||||||
) -> AuthResult<()> {
|
) -> 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)
|
.one(db)
|
||||||
.await
|
.await
|
||||||
.map_err(|e| AuthError::Validation(e.to_string()))?
|
.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()))?;
|
.ok_or_else(|| AuthError::Validation("用户不存在".to_string()))?;
|
||||||
|
|
||||||
let current_version = user_model.version;
|
let current_version = user_model.version;
|
||||||
@@ -294,11 +300,13 @@ impl UserService {
|
|||||||
db: &sea_orm::DatabaseConnection,
|
db: &sea_orm::DatabaseConnection,
|
||||||
) -> AuthResult<Vec<RoleResp>> {
|
) -> AuthResult<Vec<RoleResp>> {
|
||||||
// 验证用户存在
|
// 验证用户存在
|
||||||
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)
|
.one(db)
|
||||||
.await
|
.await
|
||||||
.map_err(|e| AuthError::Validation(e.to_string()))?
|
.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()))?;
|
.ok_or_else(|| AuthError::Validation("用户不存在".to_string()))?;
|
||||||
|
|
||||||
// 验证所有角色存在且属于当前租户
|
// 验证所有角色存在且属于当前租户
|
||||||
|
|||||||
@@ -118,9 +118,14 @@ async fn apply_rate_limit(
|
|||||||
) -> Response {
|
) -> Response {
|
||||||
let avail = redis_avail();
|
let avail = redis_avail();
|
||||||
|
|
||||||
// 快速跳过:Redis 不可达时直接放行
|
// Redis 不可达时 fail-closed:拒绝请求
|
||||||
if !avail.should_try().await {
|
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);
|
let key = format!("rate_limit:{}:{}", prefix, identifier);
|
||||||
@@ -131,17 +136,25 @@ async fn apply_rate_limit(
|
|||||||
c
|
c
|
||||||
}
|
}
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
tracing::warn!(error = %e, "Redis 连接失败,跳过限流");
|
tracing::warn!(error = %e, "Redis 连接失败,fail-closed 限流保护");
|
||||||
avail.mark_failed().await;
|
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 {
|
let count: i64 = match redis::cmd("INCR").arg(&key).query_async(&mut conn).await {
|
||||||
Ok(n) => n,
|
Ok(n) => n,
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
tracing::warn!(error = %e, "Redis INCR 失败,跳过限流");
|
tracing::warn!(error = %e, "Redis INCR 失败,fail-closed 限流保护");
|
||||||
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();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user