21-7 策略权限:数据库设计
数据权限设计背景
RBAC扩展需求
在完成基础的RBAC(基于角色的访问控制)框架后,我们面临更细粒度的权限控制需求。传统RBAC实现了用户角色→接口权限的映射,但在实际业务场景中,还需要解决以下问题:
- 数据权限控制
- 字段级访问:控制用户能否查看/修改特定字段(如薪资字段仅HR可见)
- 条件过滤:动态数据过滤(如销售员仅能查看自己负责的客户数据)
- 场景案例:
/* 数据权限SQL示例 */ SELECT * FROM orders WHERE sales_id = CURRENT_USER_ID() -- 条件过滤
sql
💡 企业ERP系统中,不同部门对同一张表需要不同的数据可见范围。
- 菜单权限分离
- 菜单权限属于前端控制范畴,与数据权限解耦
- 后续可通过角色-菜单关联表实现,避免当前设计复杂化
- 架构决策点
- 核心问题:是否需要为数据权限单独设计模型?
- ❌ 独立模型:导致权限体系碎片化,维护成本高
- ✅ 统一模型:通过关联表扩展,保持权限系统一致性
- 核心问题:是否需要为数据权限单独设计模型?
单模型双关联方案
设计原理
采用策略权限中心化设计:
技术实现优势
- 单一事实来源
- 所有权限判定基于
Policy
表,避免多模型冲突 - 修改权限时只需更新中心节点(如字段规则变更)
- 所有权限判定基于
- 关联表扩展性
- 角色-策略关联:
role_policies
表定义角色数据权限 - 路由-策略关联:
permission_policies
表定义接口准入规则 - 示例关联查询:
// 获取用户所有数据权限 const policies = await prisma.rolePolicy.findMany({ where: { roleId: user.roleId }, include: { policy: true } });
typescript
- 角色-策略关联:
- 多维度控制
控制维度 实现方式 应用场景 字段级 Policy.fields
(JSON)敏感字段动态屏蔽 行级 Policy.conditions
(SQL)数据分区(多租户)
行业实践参考
- AWS IAM:策略(Policy) + 关联实体(Principal/Resource)
- Kubernetes RBAC:RoleBinding连接角色与对象
- 最佳实践提示:
"权限系统的复杂度应集中在策略定义层,而非模型结构层"
——《Designing Data-Intensive Applications》Martin Kleppmann
💡 扩展思考:如何实现权限的继承与覆盖?可通过Policy.type
字段定义优先级规则(如1=覆盖 2=合并)
策略权限表设计详解
核心字段结构深度解析
model Policy {
id Int @id @default(autoincrement())
// 权限类型标识(1-系统权限 2-数据权限 3-功能权限)
type Int @default(1)
// 权限效果(支持allow/deny白黑名单模式)
effect String @default("allow")
// 操作类型(create/read/update/delete/manage/+自定义动作)
action String
// 资源主体(格式:模块名:资源名,如"hr:employee")
subject String
// 字段级权限配置(结构化JSON)
fields Json?
// 动态条件(支持SQL片段/表达式)
conditions Json?
// 扩展字段(审计/有效期等元数据)
x Json?
}
prisma
关键字段设计说明:
- type字段:
- 支持权限分类管理
- 典型值示例:
{ "1": "系统权限(如后台登录)", "2": "数据权限(如订单查看)", "3": "功能权限(如导出按钮)" }
json
- action字段:
- 标准操作扩展方案:
// 前端权限校验示例 const actions = { basic: ['create','read','update','delete'], extended: ['import','export','approve'] }
javascript
- 标准操作扩展方案:
- fields字段:
- 精细化控制示例:
{ "visible": ["name","department"], "editable": ["phone"], "hidden": ["salary"] }
json
- 精细化控制示例:
JSON字段高级处理方案
多数据库兼容实现
数据库类型 | 版本要求 | 存储方案 | 查询优化建议 |
---|---|---|---|
MySQL | ≥5.7 | 原生JSON | 使用->> 操作符提取JSON属性 |
PostgreSQL | ≥9.4 | JSONB(带索引支持) | 创建GIN索引加速JSON查询 |
MariaDB | 10.2+ | TEXT+CHECK约束验证JSON有效性 | 使用JSON_VALID() 函数校验 |
SQLite | ≥3.9 | TEXT+JSON1扩展 | 通过json_extract() 函数访问 |
MongoDB | 全版本 | 原生文档存储 | 使用点符号导航嵌套结构 |
低版本兼容方案
// 通用JSON处理工具函数
class JSONProcessor {
static serialize(data: object): string {
return JSON.stringify(data);
}
static parse(text: string): object {
try {
return JSON.parse(text);
} catch {
return {};
}
}
}
// 使用示例
const policy = await prisma.policy.create({
data: {
fields: JSONProcessor.serialize({
visible: ['id','name']
})
}
});
typescript
高级应用场景
动态条件引擎
// conditions字段示例
{
"ruleType": "sql",
"expression": "department_id = ${user.department}",
// 或使用表达式引擎
"ruleType": "js",
"expression": "resource.ownerId === user.id"
}
json
扩展字段(x)典型用途
{
"audit": {
"createdBy": "admin",
"createdAt": "2023-08-20"
},
"validity": {
"start": "2023-01-01",
"end": "2024-01-01"
}
}
json
Prisma最佳实践
- 类型安全增强:
// 定义JSON字段类型 type PolicyFields = { visible?: string[]; editable?: string[]; rules?: Record<string, boolean>; }; // 查询时类型转换 const policy = await prisma.policy.findFirst(); const fields = policy.fields as PolicyFields;
typescript - 索引优化:
model Policy { // 为高频查询字段创建索引 @@index([subject, action]) // JSON字段索引(PostgreSQL专有) @@index([fields(path: "$.visible")]) }
prisma
💡 行业趋势:现代权限系统正逐渐采用OPA(Open Policy Agent)等策略引擎,将权限逻辑从数据库层剥离至专用服务层。考虑未来通过conditions
字段集成Rego策略语言。
权限关联表设计详解
角色-策略关联表深度解析
model RolePolicy {
id Int @id @default(autoincrement())
roleId Int
policyId Int
// 建立外键关系
role Role @relation(fields: [roleId], references: [id])
policy Policy @relation(fields: [policyId], references: [id])
// 复合唯一约束(避免重复授权)
@@unique([roleId, policyId], name: "role_policy_unique")
// 索引优化
@@index([roleId], name: "idx_role")
@@index([policyId], name: "idx_policy")
// 映射到数据库表名
@@map("role_policies")
}
prisma
关键特性:
- 双向关系导航:
// 查询角色所有权限 const roleWithPolicies = await prisma.role.findUnique({ where: { id: 1 }, include: { policies: { include: { policy: true } } } });
typescript - 批量授权场景:
// 为角色批量添加权限 await prisma.rolePolicy.createMany({ data: [ { roleId: 1, policyId: 101 }, { roleId: 1, policyId: 102 } ] });
typescript - 权限继承实现:
可通过递归查询实现角色权限继承链
路由-策略关联表高级设计
model PermissionPolicy {
id Int @id @default(autoincrement())
permissionId Int
policyId Int
// 建立完整关系链
permission Permission @relation(fields: [permissionId], references: [id])
policy Policy @relation(fields: [policyId], references: [id])
// 带命名的复合唯一约束
@@unique([permissionId, policyId], name: "permission_policy_unique")
// 查询优化索引
@@index([permissionId], name: "idx_permission")
@@index([policyId], name: "idx_policy_ref")
// 数据库表名映射
@@map("permission_policies")
}
prisma
高级应用场景:
- 接口权限校验流程:
- 动态权限加载:
// 启动时加载路由权限映射 const routePolicies = await prisma.permissionPolicy.findMany({ include: { policy: true } }); // 转换为快速查询结构 const policyMap = new Map<number, Policy[]>(); routePolicies.forEach(rp => { policyMap.set(rp.permissionId, [...(policyMap.get(rp.permissionId) || [], rp.policy]); });
typescript
关联表性能优化方案
优化手段 | 实施方法 | 效果提升点 |
---|---|---|
读写分离 | 权限查询走从库 | 减轻主库压力 |
缓存策略 | Redis缓存热门前缀role:{roleId}:policies | 降低数据库QPS |
批量操作 | 使用createMany /updateMany | 减少网络往返 |
查询熔断 | 当权限查询超过100ms时降级为基本权限 | 保障系统可用性 |
数据分片 | 按roleId范围分表(role_policies_0 ~ role_policies_4) | 突破单表性能瓶颈 |
数据一致性保障
- 事务处理示例:
await prisma.$transaction([ prisma.rolePolicy.deleteMany({ where: { roleId: 1 } }), prisma.rolePolicy.createMany({ data: newPolicies.map(p => ({ roleId: 1, policyId: p.id })) }) ]);
typescript - 事件驱动架构:
💡 扩展思考:在微服务架构中,可以考虑将权限关联数据同步到Elasticsearch,实现复杂的联合查询和实时分析。
数据库同步与操作详解
Schema同步命令增强版
完整开发工作流
# 1. 开发环境同步(带数据填充)
npx prisma migrate dev --name init --create-only && \
npx prisma db push --accept-data-loss && \
npx prisma db seed
# 2. 生产环境同步(安全模式)
npx prisma migrate deploy && \
npx prisma generate --data-proxy
bash
关键参数说明:
命令参数 | 作用场景 | 风险控制 |
---|---|---|
--accept-data-loss | 开发环境允许字段类型变更 | 自动备份_prisma_migrations |
--create-only | 仅生成迁移文件不执行 | 适合代码评审场景 |
--skip-generate | 跳过客户端生成 | 加速CI/CD流程 |
策略CRUD完整实现
1. 增强版分页查询
// 支持排序和条件过滤的分页
const policies = await prisma.policy.findMany({
skip: (page - 1) * pageSize,
take: pageSize,
where: {
subject: { contains: 'User' },
action: { in: ['read', 'write'] }
},
orderBy: { createdAt: 'desc' },
include: {
roles: true, // 关联的角色信息
permissions: true // 关联的路由信息
}
});
// 返回分页元数据
return {
data: policies,
pagination: {
total: await prisma.policy.count({ where }),
currentPage: page,
perPage: pageSize
}
};
typescript
2. 安全策略创建
// 带输入校验的创建逻辑
const createPolicy = async (data: PolicyInput) => {
// 1. 校验action合法性
const validActions = ['create', 'read', 'update', 'delete', 'manage'];
if (!validActions.includes(data.action)) {
throw new Error(`Invalid action: ${data.action}`);
}
// 2. 防止重复策略
const exists = await prisma.policy.findFirst({
where: {
subject: data.subject,
action: data.action,
effect: data.effect
}
});
if (exists) throw new Error('Policy already exists');
// 3. 事务性创建
return await prisma.$transaction(async (tx) => {
const policy = await tx.policy.create({ data });
await tx.auditLog.create({
data: {
action: 'CREATE_POLICY',
targetId: policy.id,
userId: currentUser.id
}
});
return policy;
});
};
typescript
3. 策略更新与删除
// 带版本控制的更新
const updatePolicy = async (id: number, data: PolicyUpdate) => {
return await prisma.policy.update({
where: { id },
data: {
...data,
version: { increment: 1 }, // 乐观锁控制
updatedAt: new Date()
}
});
};
// 级联删除(需处理关联关系)
const deletePolicy = async (id: number) => {
return await prisma.$transaction([
prisma.rolePolicy.deleteMany({ where: { policyId: id } }),
prisma.permissionPolicy.deleteMany({ where: { policyId: id } }),
prisma.policy.delete({ where: { id } })
]);
};
typescript
作业实现指南
集成用户角色模块
- 权限分配接口:
// 为角色批量分配策略 const assignPolicies = async (roleId: number, policyIds: number[]) => { await prisma.rolePolicy.createMany({ data: policyIds.map(pid => ({ roleId, policyId: pid })), skipDuplicates: true // 自动跳过已存在关联 }); };
typescript - 权限校验中间件:
// Express中间件示例 const checkPermission = (requiredAction: string) => { return async (req, res, next) => { const policy = await prisma.policy.findFirst({ where: { subject: req.resourceType, action: requiredAction, roles: { some: { roleId: req.user.roleId } } } }); if (!policy) return res.status(403).end(); next(); }; }; // 使用示例 router.get('/users', checkPermission('read'), getUserList);
typescript
测试用例设计
describe('Policy CRUD', () => {
beforeAll(async () => {
await prisma.$connect();
});
it('should create policy with valid data', async () => {
const policy = await createPolicy({
effect: 'allow',
action: 'read',
subject: 'User'
});
expect(policy).toHaveProperty('id');
});
it('should reject duplicate policy', async () => {
await expect(
createPolicy({ effect: 'allow', action: 'read', subject: 'User' })
).rejects.toThrow('already exists');
});
});
typescript
性能优化技巧
- 批量操作模式:
// 使用事务批量插入 await prisma.$transaction([ prisma.policy.createMany({ data: policyList }), prisma.auditLog.createMany({ data: auditRecords }) ]);
typescript - 查询优化:
// 在schema中添加索引 model Policy { @@index([subject, action]) // 高频查询组合 @@index([effect]) // 权限校验加速 }
prisma - 缓存策略:
// Redis缓存权限数据 const cachedPolicies = await redis.get(`policies:${roleId}`); if (!cachedPolicies) { const policies = await prisma.policy.findMany({...}); await redis.setEx(`policies:${roleId}`, 3600, JSON.stringify(policies)); }
typescript
💡 扩展建议:结合Prisma的$queryRaw
实现复杂条件的数据权限过滤,如多租户数据隔离:
const userData = await prisma.$queryRaw`
SELECT * FROM ${raw(tableName)}
WHERE ${raw(conditionSQL)}
AND tenant_id = ${user.tenantId}
`;
typescript
设计注意事项深度解析
版本兼容方案增强版
多版本数据库适配策略
具体实现方案对比
方案类型 | 优点 | 缺点 | 适用场景 |
---|---|---|---|
应用层处理 | 兼容性最好 | 查询性能差 | 老旧系统迁移期 |
数据库原生JSON | 查询性能最优 | 需要现代数据库支持 | 新建系统 |
混合模式 | 平衡兼容性与性能 | 实现复杂度高 | 渐进式升级项目 |
代码实现示例:
// 智能字段处理器
class JSONFieldHandler {
constructor(private dbType: string, private dbVersion: number) {}
prepareValue(data: object): any {
if (this.supportsNativeJSON()) {
return data;
}
return JSON.stringify(data);
}
parseValue(value: any): object {
if (this.supportsNativeJSON()) {
return value;
}
return JSON.parse(value);
}
private supportsNativeJSON(): boolean {
const versionMap = {
mysql: 5.7,
postgresql: 9.4,
sqlite: 3.9
};
return this.dbVersion >= versionMap[this.dbType];
}
}
typescript
扩展优化建议完整方案
1. 字段增强实战指南
model Policy {
// ...其他字段...
description String? @db.VarChar(500) // 权限说明文档
action String @default("read") // 使用枚举类型更规范
@@enum([
"create",
"read",
"update",
"delete",
"manage",
"import",
"export"
])
}
prisma
字段设计建议:
- 为
description
添加全文索引支持模糊搜索 - 对
action
枚举值建立字典表便于国际化
2. 索引优化高级技巧
model Policy {
// 多列组合索引
@@index([subject, action, effect], name: "idx_security_check")
// 条件索引(PostgreSQL特有)
@@index([subject], name: "idx_active_subjects", where: "effect = 'allow'")
// JSON字段索引
@@index([fields(path: "$.visible")], name: "idx_visible_fields")
}
prisma
索引优化原则:
- 遵循最左前缀原则设计组合索引
- 对WHERE子句中的高频条件创建条件索引
- 对JSON中的热点路径创建函数索引
3. 审计跟踪完整实现
model Policy {
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
createdBy String
updatedBy String
}
model PolicyHistory {
id Int @id @default(autoincrement())
policyId Int
operation String // "CREATE|UPDATE|DELETE"
oldData Json?
newData Json?
changedAt DateTime @default(now())
changedBy String
@@index([policyId])
@@index([changedAt])
}
prisma
审计日志方案对比:
方案 | 优点 | 缺点 |
---|---|---|
触发器记录 | 对应用透明 | 跨数据库兼容性差 |
ORM Hook记录 | 灵活可控 | 增加应用复杂度 |
事件溯源模式 | 完整历史追溯 | 存储空间消耗大 |
企业级实践建议
- 数据权限演进路线:
- 合规性要求:
- 保留至少6个月的权限变更记录(ISO27001要求)
- 实现权限修改的双因素审批流程
- 定期进行权限审计报表(每月生成
权限矩阵报告
)
- 性能监控指标:
- 权限校验平均延迟(P99 < 50ms)
- 策略缓存命中率(目标 > 95%)
- 权限表数据增长率(预警阈值 > 10%/月)
💡 专家建议:在Kubernetes环境中考虑将权限策略配置为CRD(Custom Resource Definition),实现声明式的权限管理。同时推荐使用Open Policy Agent进行策略解耦。
↑