Files
junhong_cmp_fiber/openspec/changes/archive/2026-01-16-implement-permission-check/design.md
huang 028cfaa7aa feat: 实现权限检查功能并添加Redis缓存优化
- 完成 CheckPermission 方法的完整实现(账号→角色→权限查询链)
- 实现 Redis 缓存机制,大幅提升权限查询性能(~12倍提升)
- 自动缓存失效:角色/权限变更时清除相关用户缓存
- 新增完整的单元测试和集成测试(10个测试用例全部通过)
- 添加权限检查使用文档和缓存机制说明
- 归档 implement-permission-check OpenSpec 提案

性能优化:
- 首次查询: ~18ms(3次DB查询 + 1次Redis写入)
- 缓存命中: ~1.5ms(1次Redis查询)
- TTL: 30分钟,自动失效机制保证数据一致性
2026-01-16 18:15:32 +08:00

238 lines
7.5 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.
# Technical Design: 权限检查服务实现
## Context
当前权限系统已实现:
- ✅ RBAC 数据模型Account - Role - Permission 多对多关联)
- ✅ Store 层AccountRoleStore, RolePermissionStore, PermissionStore
- ✅ 权限中间件框架(`pkg/middleware/permission.go`
-**权限检查核心逻辑缺失**`CheckPermission` 方法仅为占位)
**目标**:补全权限检查服务,使权限中间件能够正常工作。
**约束**
- 严格遵循项目四层架构Handler → Service → Store → Model
- 不引入外部依赖或框架
- 保持 Go 惯用模式,避免过度抽象
## Goals / Non-Goals
### Goals
1. 实现完整的权限检查逻辑(账号 → 角色 → 权限链式查询)
2. 支持 platform 参数过滤all/web/h5
3. 超级管理员自动通过所有权限检查
4. 提供单元测试和集成测试覆盖
### Non-Goals
1. ❌ 不实现基于资源的权限RBAC 仅支持功能权限)
2. ❌ 不实现权限继承或权限组(保持简单)
3. ❌ 不实现动态权限(权限在数据库中静态配置)
## Decisions
### Decision 1: 依赖注入方式
**选择**: 在 `PermissionService` 结构体中注入 `AccountRoleStore``RolePermissionStore`
**理由**:
- ✅ 符合项目依赖注入模式(通过结构体字段注入)
- ✅ 避免循环依赖Service 依赖 StoreStore 不依赖 Service
- ✅ 便于单元测试(可 mock Store 层)
**替代方案**:
- ❌ 方案 A: 在 PermissionStore 中添加聚合查询方法
- 缺点:违反 Store 层单一职责原则
- ❌ 方案 B: 使用全局变量或服务定位器
- 缺点:违反项目依赖注入原则
### Decision 2: 超级管理员权限处理
**选择**: 在 `CheckPermission` 方法开头检查用户类型,超级管理员直接返回 true
**理由**:
- ✅ 性能优化:避免无意义的数据库查询
- ✅ 业务语义:超级管理员拥有所有权限
- ✅ 与现有数据权限逻辑一致(数据权限也跳过超级管理员)
**实现**:
```go
func (s *Service) CheckPermission(ctx, userID, permCode, platform) (bool, error) {
// 1. 检查用户类型(需要从 context 或通过 AccountStore 查询)
userType := middleware.GetUserTypeFromContext(ctx)
if userType == constants.UserTypeSuperAdmin {
return true, nil
}
// 2. 常规权限检查流程
// ...
}
```
### Decision 3: Platform 匹配逻辑
**选择**: 权限的 `platform` 字段支持三种值:`all`(任意端),`web`(仅后台),`h5`(仅 H5
**匹配规则**:
```go
// 权限匹配条件:
// 1. permCode 完全匹配
// 2. platform 匹配:
// - permission.platform == "all" → 任意 platform 参数都匹配
// - permission.platform == platform → 精确匹配
```
**示例**:
```
查询权限: CheckPermission(userID, "user:create", "web")
权限数据库:
- {permCode: "user:create", platform: "all"} → ✅ 匹配
- {permCode: "user:create", platform: "web"} → ✅ 匹配
- {permCode: "user:create", platform: "h5"} → ❌ 不匹配
```
### Decision 4: 缓存策略(可选实现)
**选择**: 第一阶段**不实现**缓存,保持简单
**理由**:
- ✅ 权限查询频率不高(仅在请求进入时检查一次)
- ✅ 权限数据量小(通常 < 100 条权限,< 10 个角色/用户)
- ✅ PostgreSQL 查询性能足够3 次简单查询 < 10ms
- ✅ 避免缓存失效复杂性(角色变更时需清除所有关联用户缓存)
**未来优化方向**:
- 如果性能测试发现瓶颈,可添加 Redis 缓存
- 缓存粒度:`permission:user:{userID}:perms``[]string` (权限编码列表)
- 过期时间5 分钟
- 失效策略:角色分配/权限分配变更时,清除相关用户缓存
## Implementation Details
### 核心算法流程
```go
func (s *Service) CheckPermission(ctx context.Context, userID uint, permCode string, platform string) (bool, error) {
// 步骤 1: 检查超级管理员
userType := middleware.GetUserTypeFromContext(ctx)
if userType == constants.UserTypeSuperAdmin {
return true, nil
}
// 步骤 2: 查询用户的角色 ID 列表
roleIDs, err := s.accountRoleStore.GetRoleIDsByAccountID(ctx, userID)
if err != nil {
return false, fmt.Errorf("failed to get user roles: %w", err)
}
if len(roleIDs) == 0 {
return false, nil // 用户无角色,无权限
}
// 步骤 3: 查询角色的权限 ID 列表(去重)
permIDs, err := s.rolePermStore.GetPermIDsByRoleIDs(ctx, roleIDs)
if err != nil {
return false, fmt.Errorf("failed to get role permissions: %w", err)
}
if len(permIDs) == 0 {
return false, nil // 角色无权限
}
// 步骤 4: 查询权限详情
permissions, err := s.permissionStore.GetByIDs(ctx, permIDs)
if err != nil {
return false, fmt.Errorf("failed to get permissions: %w", err)
}
// 步骤 5: 遍历匹配 permCode 和 platform
for _, perm := range permissions {
if perm.PermCode == permCode {
// platform 匹配规则
if perm.Platform == constants.PlatformAll || perm.Platform == platform {
return true, nil
}
}
}
return false, nil // 未找到匹配的权限
}
```
### 数据库查询分析
**查询次数**: 3 次
1. `AccountRoleStore.GetRoleIDsByAccountID()` - 1 次查询
2. `RolePermissionStore.GetPermIDsByRoleIDs()` - 1 次查询(带去重)
3. `PermissionStore.GetByIDs()` - 1 次查询(批量)
**预估性能**:
- 单次权限检查 < 10ms本地数据库
- 单次权限检查 < 20ms远程数据库
**优化空间**:
- 如果未来需要,可添加缓存层减少查询次数
- 数据库索引已存在(`account_id`, `role_id`, `perm_id`
## Risks / Trade-offs
### Risk 1: 多次数据库查询影响性能
**影响**: 每次权限检查需要 3 次数据库查询
**缓解**:
- ✅ 查询简单,使用索引,性能可接受
- ✅ 权限检查仅在请求入口执行一次(不在业务逻辑中频繁调用)
- ✅ 如果未来需要,可添加缓存层
**监控**:
- 在日志中记录权限检查耗时
- 生产环境监控 API 响应时间
### Risk 2: 角色或权限变更后权限立即生效
**现状**: 不使用缓存,权限变更立即生效
**影响**:
- ✅ 无缓存一致性问题
- ❌ 性能稍低(可接受)
**未来优化**:
- 如果添加缓存,需实现缓存失效机制
## Migration Plan
### 部署步骤
1. **代码部署**:
- 合并代码到主分支
- 部署 API 服务
2. **验证**:
- 运行集成测试
- 手动测试权限中间件
3. **激活权限中间件**(可选):
- 在需要权限控制的路由上添加 `RequirePermission` 中间件
- 逐步启用,监控错误率
### Rollback Plan
- ✅ 无破坏性变更,可直接回滚代码
- ✅ 权限中间件默认未启用,不影响现有功能
## Open Questions
1. **是否需要添加权限检查日志审计?**
- 当前设计:仅在错误时记录日志
- 可选:记录所有权限检查结果(包括成功)用于安全审计
- **决策**: 暂不实现,避免日志量过大
2. **是否需要支持权限继承?**
- 当前设计:权限扁平化,不支持继承
- 可选:支持父级权限自动包含子级权限
- **决策**: 暂不实现,保持简单
3. **是否需要支持权限否定(黑名单)?**
- 当前设计:仅支持白名单(有权限才能访问)
- 可选:支持明确拒绝某些权限
- **决策**: 暂不实现,通过角色分配控制即可