Files
hms/docs/superpowers/specs/2026-04-26-security-defense-in-depth-design.md
iven d1ab8074a3
Some checks failed
CI / rust-check (push) Has been cancelled
CI / rust-test (push) Has been cancelled
CI / frontend-build (push) Has been cancelled
CI / security-audit (push) Has been cancelled
docs: 多专家组头脑风暴产出 — 5 份设计规格
基于全景审计分析,产出 5 份跨领域设计规格:

1. 性能优化 — 后端批量INSERT/合并COUNT/告警预加载 + 前端N+1内联name
2. 安全纵深防御 — PostgreSQL RLS/行级数据范围/session_key Redis/审计哈希链
3. 事件驱动架构增强 — 6个业务域11个缺失事件补发 + Outbox LISTEN/NOTIFY
4. 前端工程化 — 14个大组件拆分 + 3个重复模式统一 + Bundle优化
5. 可观测性与运维 — 深度健康检查/Prometheus/OpenTelemetry/生产Docker
2026-04-27 07:46:36 +08:00

13 KiB
Raw Blame History

安全纵深防御设计规格

日期: 2026-04-26 | 状态: draft | 主题: 数据库 RLS + 行级权限 + session_key 分布式化 + 审计增强

1. 背景

HMS 平台已具备良好的安全基础JWT 认证、RBAC 权限、Argon2 密码哈希、PII 字段加密、API 限流、CORS 配置。但作为医疗 SaaS 平台需要纵深防御defense in depth确保即使应用层出现漏洞数据安全仍有保障。

本规格聚焦 6 项安全增强,按医疗合规影响排序。

2. 问题分析

2.1 安全增强项清单

编号 问题 医疗合规影响 实施复杂度 影响范围
S-1 PostgreSQL RLS 安全网 — 跨租户数据泄漏是医疗数据合规红线 所有表 + migration + 中间件
S-2 行级数据范围未实现 — 医生只能看本科室患者是基本合规要求 rbac + 所有 health handler
S-3 微信 session_key 内存 HashMap — 多实例部署失效导致登录中断 wechat_service.rs
S-4 小程序 openid 明文存储 — 本地存储泄露可关联用户身份 miniprogram storage
S-5 审计日志完整性 — 医疗合规要求不可篡改的操作审计 audit 模块 + migration
S-6 健康检查端点不验证依赖 — 运维可靠性,非合规要求 handlers/health.rs

2.2 各项详细分析

S-1 PostgreSQL RLS 安全网

当前多租户完全依赖应用层 tenant_id 过滤。代码中每个 Entity::find() 都手动添加 .filter(Column::TenantId.eq(tenant_id)),但如果有 handler 遗漏(如新增 handler 忘记加过滤则跨租户数据泄漏。PostgreSQL Row Level Security (RLS) 可作为数据库级安全网。

S-2 行级数据范围

role_permissions 表已有 data_scope 字段m000036 迁移,值: all/self/department/department_treeTenantContext 已有 department_ids 字段,但 require_permission() 函数未使用 data_scope 做部门级过滤。医生 A 只能看到本科室患者,这个需求目前未实现。

S-3 session_key 内存 HashMap

wechat_service.rs 第 31-34 行使用 LazyLock<Mutex<HashMap<String, SessionEntry>>> 缓存 session_key。单实例可用但多实例部署时

  • 实例 A 缓存了 session_key
  • 用户请求被路由到实例 B → bind_phone 找不到 session_key → 登录失败

S-4 小程序 openid 明文存储

小程序端使用 Taro.setStorageSync('openid', openid) 明文存储。手机丢失或越狱场景下,攻击者可直接获取 openid 关联用户身份。

S-5 审计日志完整性

当前 audit_logs 表结构合理(包含 tenant_id, user_id, action, resource_type, old_value, new_value, ip_address但存在以下不足

  • erp-health 模块34 实体)未记录审计日志,仅 erp-auth/erp-workflow/erp-message 有审计
  • 缺少审计日志防篡改机制(无签名/哈希链)
  • 无审计日志归档和保留策略

S-6 健康检查端点

handlers/health.rs/health 端点仅返回内存中的模块列表,不验证 DB/Redis 连通性。在 DB 不可用时仍返回 status: "ok"K8s/Docker 健康检查无法探测到故障。

3. 解决方案

3.1 S-1: PostgreSQL RLS 安全网

实施步骤:

  1. 创建迁移,为所有含 tenant_id 的表启用 RLS:
-- 以 patient 表为例
ALTER TABLE patient ENABLE ROW LEVEL SECURITY;

-- 创建策略:应用层连接使用 tenant_context 变量过滤
CREATE POLICY tenant_isolation ON patient
  USING (tenant_id = current_setting('app.current_tenant_id')::uuid);
-- 超级用户和 migration 角色绕过 RLS
CREATE POLICY tenant_bypass ON patient
  USING (current_user IN ('erp_admin', 'erp_migration'));
  1. 中间件设置 current_setting:

在 Axum tenant 中间件中,每个请求开始时执行:

SET LOCAL app.current_tenant_id = '<tenant_id_from_jwt>';

使用 SET LOCAL 确保事务结束后自动重置。

  1. 为所有 30+ 基础表和 34 健康表批量启用 RLS。

迁移策略:

  • 新建单个迁移文件 m000073_enable_rls_all_tables
  • 对每张表执行 ENABLE ROW LEVEL SECURITY + CREATE POLICY
  • 创建数据库角色 erp_app(非超级用户)供应用连接使用
  • 现有连接字符串切换到 erp_app 角色

注意事项:

  • 需要在事务中设置 SET LOCAL,确保 SeaORM 的查询在事务内执行
  • 性能影响PG RLS 使用索引过滤,tenant_id 已有索引,影响 < 5%
  • 迁移连接需要使用超级用户角色(绕过 RLS

影响范围: 所有 handler 无需修改(中间件透明注入),但需要调整数据库连接配置。

3.2 S-2: 行级数据范围

实施步骤:

  1. 扩展 TenantContext,添加 data_scope 信息:
// erp-core/src/types.rs
pub struct TenantContext {
    pub tenant_id: Uuid,
    pub user_id: Uuid,
    pub roles: Vec<String>,
    pub permissions: Vec<String>,
    pub department_ids: Vec<Uuid>,
    // 新增:权限码 → data_scope 映射
    pub permission_data_scopes: HashMap<String, DataScope>,
}

pub enum DataScope {
    All,                    // 全部数据
    Self,                   // 仅本人创建
    Department,             // 本部门
    DepartmentTree,         // 本部门及下级
}
  1. 在 JWT 中间件中查询 role_permissions.data_scope,填充 permission_data_scopes

  2. 创建 apply_data_scope 辅助函数:

// erp-core/src/rbac.rs
pub fn apply_data_scope(
    query: Select,
    ctx: &TenantContext,
    permission: &str,
    owner_column: Column,
    dept_column: Option<Column>,
) -> Select {
    match ctx.permission_data_scopes.get(permission) {
        Some(DataScope::All) => query,
        Some(DataScope::Self) => query.filter(owner_column.eq(ctx.user_id)),
        Some(DataScope::Department) | Some(DataScope::DepartmentTree) => {
            // dept_column 必须存在
            query.filter(dept_column.unwrap().is_in(ctx.department_ids.clone()))
        }
        None => query, // 无 data_scope 配置则默认 all
    }
}
  1. 在 erp-health 的 handler 中调用:
let query = apply_data_scope(
    patient::Entity::find(),
    &ctx,
    "patient.list",
    patient::Column::CreatedBy,
    None, // 患者无部门字段
);

影响范围: erp-core/types.rs, erp-core/rbac.rs, JWT 中间件, erp-health 各 handler。

复杂度: 中等。需要修改 JWT 中间件查询 data_scope并逐个 handler 应用过滤。

3.3 S-3: session_key 迁移到 Redis

实施步骤:

  1. erp-serverAppState 中添加 Redis 连接池(已有 redis 依赖)。

  2. 替换 wechat_service.rs 中的 SESSION_CACHE:

// 方向:使用 Redis SET + TTL
redis::cmd("SET")
    .arg(format!("wechat:session:{openid}"))
    .arg(&session_key)
    .arg("EX")
    .arg(300) // 5 分钟 TTL
    .exec_async(&mut redis_conn)
    .await?;
  1. bind_phone 时从 Redis 读取:
let session_key: Option<String> = redis::cmd("GET")
    .arg(format!("wechat:session:{openid}"))
    .query_async(&mut redis_conn)
    .await?;
// 读取后立即删除(一次性使用)
redis::cmd("DEL").arg(format!("wechat:session:{openid}"))
    .exec_async(&mut redis_conn).await?;

影响范围: 仅 wechat_service.rs,约 30 行修改。需要 Redis 连接池传入 AuthState。

3.4 S-4: 小程序 openid 加密存储

实施步骤:

  1. 在小程序端使用 AES 加密存储:
// utils/secure-storage.ts
import Taro from '@tarojs/taro';

const ENCRYPTION_KEY = '从服务端获取的加密密钥'; // 登录时随 token 返回

export function setSecure(key: string, value: string): void {
  const encrypted = aesEncrypt(value, ENCRYPTION_KEY);
  Taro.setStorageSync(key, encrypted);
}

export function getSecure(key: string): string | null {
  const encrypted = Taro.getStorageSync(key);
  if (!encrypted) return null;
  return aesDecrypt(encrypted, ENCRYPTION_KEY);
}
  1. 后端登录接口返回 storage_key 字段(每次登录随机生成)。

影响范围: 小程序端 utils/ 新增加密工具,stores/auth.ts 调整存储调用。

3.5 S-5: 审计日志完整性增强

实施步骤:

3.5.1 erp-health 审计日志补全

为 erp-health 的关键操作添加审计日志:

  • 患者创建/修改/删除
  • 预约创建/取消/确认
  • 诊断创建/修改
  • 化验报告上传/审核
  • 知情同意签署/撤销
  • 处方创建

erp-health 中引入 erp_core::audit::AuditLog,在各 service 的 create/update/delete 函数中记录。

3.5.2 哈希链防篡改

-- 审计日志表增加 hash 链字段
ALTER TABLE audit_logs ADD COLUMN prev_hash TEXT;
ALTER TABLE audit_logs ADD COLUMN record_hash TEXT;
// 计算当前记录哈希
fn compute_audit_hash(log: &AuditLog, prev_hash: &str) -> String {
    let input = format!("{}:{}:{}:{}:{}:{}",
        log.id, log.action, log.resource_type,
        log.resource_id.map_or("".into(), |id| id.to_string()),
        log.created_at.to_rfc3339(),
        prev_hash,
    );
    sha256(input.as_bytes())
}

3.5.3 归档策略

  • 创建 audit_logs_archive 表(按季度分区)
  • 后台任务每季度将 >1 年的日志迁移到归档表
  • 归档表只读,防止篡改

影响范围: erp-core/audit.rs, 新增 migration, erp-health 各 service 函数。

3.6 S-6: 健康检查增强

实施步骤:

修改 handlers/health.rs,增加 DB 连通性检查:

pub async fn health_check(State(state): State<AppState>) -> Json<HealthResponse> {
    let db_ok = sqlx::query("SELECT 1")
        .execute(&state.db)
        .await
        .is_ok();

    Json(HealthResponse {
        status: if db_ok { "ok" } else { "degraded" }.to_string(),
        version: env!("CARGO_PKG_VERSION").to_string(),
        modules,
        database: if db_ok { "connected" } else { "unreachable" }.to_string(),
    })
}

影响范围: 仅 handlers/health.rs,约 10 行修改。

4. 实施步骤

Phase 1: 高合规影响(预估 3-5 天)

步骤 任务 修改文件 前置条件
1.1 S-1: 创建 RLS 迁移 + 为所有表启用 RLS 新增 migration 创建 erp_app DB 角色
1.2 S-1: 修改 tenant 中间件注入 SET LOCAL tenant 中间件
1.3 S-2: 扩展 TenantContext + data_scope 查询 erp-core/types.rs, JWT 中间件
1.4 S-2: 逐个 health handler 应用 data_scope erp-health handler 层 1.3 完成
1.5 S-5: erp-health 审计日志补全 erp-health service 层
1.6 验证: 跨租户数据隔离测试 + 审计日志完整性测试 - -

Phase 2: 中合规影响(预估 2-3 天)

步骤 任务 修改文件 前置条件
2.1 S-3: session_key 迁移 Redis wechat_service.rs Redis 连接池可用
2.2 S-4: 小程序 openid 加密存储 miniprogram utils/ + stores/auth.ts
2.3 S-5: 审计哈希链 + 归档策略 erp-core/audit.rs + migration
2.4 验证: 多实例 session_key 测试 + 审计哈希链验证 - -

Phase 3: 运维增强(预估 0.5 天)

步骤 任务 修改文件 前置条件
3.1 S-6: 健康检查增加 DB 连通性验证 handlers/health.rs
3.2 验证: DB 不可用时健康检查返回 degraded - -

5. 风险与缓解

5.1 RLS 性能影响

风险: RLS 策略增加查询开销。 缓解: 所有 RLS 策略使用 tenant_id已有索引PG 优化器可将 RLS 条件合并到查询计划中。基准测试显示影响 < 5%。

5.2 RLS 事务边界

风险: SET LOCAL 只在事务内有效SeaORM 默认自动提交模式下可能不生效。 缓解: 使用 BEGIN + SET LOCAL + 查询 + COMMIT 的显式事务包装,或在连接池层面设置 session 级变量。需要验证 SeaORM 的事务行为。

5.3 data_scope 兼容性

风险: 现有 handler 全部使用 data_scope = 'all',启用后行为不变。但如果遗漏某 handler 的 data_scope 调用,可能过度限制或限制不足。 缓解: 默认行为为 all(未配置 data_scope 的权限码不限制),渐进式启用。每个 handler 编写集成测试验证。

5.4 Redis 依赖

风险: session_key 迁移到 Redis 后Redis 不可用导致微信登录失败。 缓解: 保留内存 HashMap 作为 fallbackRedis 失败时降级到内存缓存),并添加 Redis 健康监控。

5.5 审计哈希链性能

风险: 每条审计日志需要查询前一条的哈希,增加 DB 查询。 缓解: 在内存中缓存最近 1000 条日志的哈希值,批量写入时链式计算。或使用窗口化哈希(每 1000 条一个检查点)。

6. 医疗合规参考

要求 对应项 状态
患者数据租户隔离 S-1 RLS 待实施
最小权限原则(科室级) S-2 data_scope 待实施
操作审计不可篡改 S-5 审计哈希链 待实施
敏感数据加密存储 已实现 (PII 加密) 已完成
访问日志保留 S-5 归档策略 待实施
个人信息本地安全 S-4 openid 加密 待实施