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

363 lines
13 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 安全纵深防御设计规格
> 日期: 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_tree`TenantContext` 已有 `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:
```sql
-- 以 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'));
```
2. 中间件设置 `current_setting`:
在 Axum tenant 中间件中,每个请求开始时执行:
```sql
SET LOCAL app.current_tenant_id = '<tenant_id_from_jwt>';
```
使用 `SET LOCAL` 确保事务结束后自动重置。
3. 为所有 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` 信息:
```rust
// 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, // 本部门及下级
}
```
2. 在 JWT 中间件中查询 `role_permissions.data_scope`,填充 `permission_data_scopes`
3. 创建 `apply_data_scope` 辅助函数:
```rust
// 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
}
}
```
4. 在 erp-health 的 handler 中调用:
```rust
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-server``AppState` 中添加 Redis 连接池(已有 `redis` 依赖)。
2. 替换 `wechat_service.rs` 中的 `SESSION_CACHE`:
```rust
// 方向:使用 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?;
```
3. `bind_phone` 时从 Redis 读取:
```rust
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 加密存储:
```typescript
// 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);
}
```
2. 后端登录接口返回 `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 哈希链防篡改**
```sql
-- 审计日志表增加 hash 链字段
ALTER TABLE audit_logs ADD COLUMN prev_hash TEXT;
ALTER TABLE audit_logs ADD COLUMN record_hash TEXT;
```
```rust
// 计算当前记录哈希
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 连通性检查:
```rust
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 加密 | 待实施 |