外观
15 · RBAC 权限系统升级方案
把当前"单一 role 字段 + URL 前缀拦截"的简化版权限模型,升级为标准的 用户-角色-权限 三段式 RBAC,支持多角色、按钮级权限、数据级隔离与审计追溯。
当前状态:方案已设计,尚未实现。现有系统使用
sys_user.role单值字段(admin/user)做基础权限控制,RBAC 四表升级为后续优化方向。
1. 背景与目标
1.1 现状痛点
| 维度 | 现状 | 问题 |
|---|---|---|
| 用户-角色 | sys_user.role 单值 VARCHAR | 一个用户只能有一个角色,无法表达"知识库管理员 + 普通对话用户" |
| 角色-权限 | 没有权限概念,角色字符串 = 权限 | hasRole('ADMIN') 是粗粒度二元判断;新增子角色需改动代码 |
| 鉴权点 | URL 前缀 /api/admin/** + 散落的 @PreAuthorize | 多处硬编码 'ADMIN' 字面量,权限点无法集中管理 |
| 数据级 | 无 | 用户能拿到他人的会话历史 / 上传的知识库(仅靠业务代码自查 user_id,且并非全部接口都做了) |
| 注册流程 | user.setRole("user") 硬编码(UserServiceImpl.java:108) | 角色取值无白名单校验,updateUserRole 接受任意字符串 |
| 前端 | userStore.isAdmin 仅控制侧边栏显隐 | 路由守卫只校验登录态,不校验 requiresAdmin(router/index.ts:120) |
| 审计 | 无 | 角色变更无日志;JWT 内 role 缓存到过期,权限变更不能即时生效 |
1.2 改造目标
- 支持多角色:一个用户可挂多个角色(如"知识库管理员 + 数据分析员")
- 细粒度权限点:以
资源:动作命名(kb:upload、mcp:execute),鉴权点不再写ADMIN字面量 - 按钮级前端控制:前端拿到权限码列表,渲染时控制按钮可见性
- 数据级隔离:知识库、会话历史按 owner 隔离,跨用户访问需要显式权限
- 可审计、可热更新:角色变更落表,JWT 失效后下次刷新即生效,无需重启
2. 总体架构
2.1 五张表的经典 RBAC 模型
sys_user ──┬── sys_user_role ──┬── sys_role ──┬── sys_role_permission ──┬── sys_permission
└── (多对多) └── (多对多)| 表名 | 作用 | 关键字段 |
|---|---|---|
sys_user | 用户(保留) | id, username, password, status —— 删除 role 字段,迁移到关联表 |
sys_role | 角色 | id, code(唯一), name, description, is_builtin(系统角色不可删), status |
sys_permission | 权限点 | id, code(唯一,如 kb:upload), name, type(menu/button/api), resource, action, parent_id |
sys_user_role | 用户-角色 | user_id, role_id, grant_time, granted_by |
sys_role_permission | 角色-权限 | role_id, perm_id |
2.2 权限点设计(基于现有 7 个 controller 的盘点)
| 模块 | 权限码 | 含义 | 默认归属 |
|---|---|---|---|
| 知识库 | kb:read | 查看知识库列表/详情 | USER, KB_ADMIN, SUPER_ADMIN |
kb:upload | 上传文档/添加 URL | KB_ADMIN, SUPER_ADMIN | |
kb:delete | 删除知识库 | KB_ADMIN, SUPER_ADMIN | |
kb:reprocess | 重新切片/刷新 | KB_ADMIN, SUPER_ADMIN | |
| 对话 | chat:use | 使用对话/SSE 流 | USER, KB_ADMIN, SUPER_ADMIN |
chat:export | 导出会话 | USER, KB_ADMIN, SUPER_ADMIN | |
| MCP | mcp:read | 查看工具列表/统计 | KB_ADMIN, SUPER_ADMIN |
mcp:execute | 在线测试工具 | SUPER_ADMIN | |
mcp:admin | 启停工具 | SUPER_ADMIN | |
| AI 配置 | ai-config:read | 查看 RAG 参数 | KB_ADMIN, SUPER_ADMIN |
ai-config:write | 修改 RAG 参数 | SUPER_ADMIN | |
| 用户 | user:read | 查看用户列表 | SUPER_ADMIN |
user:manage | 改状态/改角色 | SUPER_ADMIN | |
| 统计 | stats:read | 查看 dashboard | KB_ADMIN, SUPER_ADMIN |
| 审计 | audit:read | 查看操作日志 | SUPER_ADMIN |
2.3 内置角色
| 角色码 | 名称 | 典型场景 |
|---|---|---|
SUPER_ADMIN | 超级管理员 | 全权限,迁移时 role='admin' 的用户自动归入 |
KB_ADMIN | 知识库管理员 | 管文档、看统计、调 RAG 参数,但不能管理用户 |
USER | 普通用户 | 仅对话 + 查看知识库;迁移时 role='user' 的用户归入 |
3. 鉴权流程改造
3.1 JWT claim:从 role 字符串改为 permission 列表
改造前(JwtUtils.java:36):
java
claims.put("role", role); // 单值 "admin"改造后:
java
claims.put("roles", List.of("KB_ADMIN", "USER")); // 角色码(用于显示)
claims.put("perms", List.of("kb:upload", "kb:delete")); // 权限码(用于鉴权)权限码列表在登录时由 PermissionService.loadPermissionCodes(userId) 一次查出后写入 token,避免每次请求查库。
3.2 UserDetailsService 改造
改造前(UserDetailsServiceImpl.java:44):
java
authorities(List.of(new SimpleGrantedAuthority("ROLE_" + user.getRole().toUpperCase())))改造后:
java
List<String> perms = permissionService.loadPermissionCodes(user.getId());
List<GrantedAuthority> authorities = perms.stream()
.map(SimpleGrantedAuthority::new) // 直接用 "kb:upload" 不加前缀
.collect(Collectors.toList());3.3 鉴权注解:从 hasRole 改为 hasAuthority
java
// 改造前
@PreAuthorize("hasRole('ADMIN')")
// 改造后
@PreAuthorize("hasAuthority('kb:upload')")为避免散落字面量,引入常量类 Perms.KB_UPLOAD = "kb:upload",并允许用 SpEL 组合:
java
@PreAuthorize("hasAnyAuthority('kb:upload','kb:delete')")3.4 数据级权限:Owner 校验切面
新增 @OwnerCheck 注解 + AOP 切面,对带 kbId / conversationId 入参的接口统一校验:
java
@OwnerCheck(resource = "kb", idParam = "id")
@DeleteMapping("/{id}")
public Result delete(@PathVariable Long id) { ... }切面逻辑:
- 当前用户拥有
kb:admin:any权限 → 放行 - 否则查
kb_knowledge_base.user_id == 当前用户→ 放行 - 否则抛
AccessDeniedException
3.5 权限热更新
- 角色/权限变更时,向 Redis 写
user:perm:revoke:{userId}={timestamp} JwtAuthenticationFilter校验 token 时,比对 token 签发时间与 revoke 时间戳,若 token 早于 revoke → 强制 401,触发前端用 refresh token 重新拿 token
4. 落地步骤(4 个 Phase)
Phase 1:数据模型 & 元数据初始化
- 在
docs/docmind.sql新增 4 张表 DDL(sys_role、sys_permission、sys_user_role、sys_role_permission) - 数据迁移脚本:
- 内置角色与权限点 INSERT
sys_user.role='admin'→ 写入sys_user_role(user_id, SUPER_ADMIN.id)sys_user.role='user'→ 写入sys_user_role(user_id, USER.id)sys_user.role字段保留 1 个版本作为回滚兜底,下次大版本删除
- 实体类:新增
SysRole、SysPermission、SysUserRole、SysRolePermission+ Mapper
Phase 2:后端鉴权改造
- 新增
PermissionService.loadPermissionCodes(userId),带 Redis 缓存(key=perm:user:{id},TTL 30min) - 改造
JwtUtils:写入/读取permsclaim - 改造
UserDetailsServiceImpl:把 perm 码作为 authority - 改造
SecurityConfig:删除.requestMatchers("/api/admin/**").hasRole("ADMIN"),全部下沉到@PreAuthorize - 重写所有 controller 的注解:
- KnowledgeBaseController.java →
kb:* - McpConsoleController.java →
mcp:* - AiConfigController.java →
ai-config:* - UserController.java →
user:*+ 自我操作放行 - DocMindStatsController.java →
stats:read
- KnowledgeBaseController.java →
- 新增
RoleManageController(/api/admin/role):CRUD 角色、给角色配权限、给用户配角色
Phase 3:前端按钮级权限
- 后端登录返回值新增
permissions: string[] stores/user.ts增加hasPerm(code: string): boolean- 路由守卫补全
requiresAdmin / requiresPerm校验(修复现有 router/index.ts 的"防君子不防小人"漏洞) - 自定义指令
v-perm:
vue
<el-button v-perm="'kb:delete'" @click="del">删除</el-button>- 新增页面:
views/admin/role/(角色列表 + 配权限抽屉)
Phase 4:数据级权限 + 审计
- 新增
@OwnerCheck注解 +OwnerCheckAspect切面 - 新增
sys_audit_log表,AOP 切面记录登录/角色变更/权限变更 - 知识库、会话相关查询 SQL 加
user_id隐式过滤(除非有*:any权限)
5. 关键改造文件清单
| 文件 | 改造类型 |
|---|---|
docs/docmind.sql | 新增 4 张表 + 内置数据 + 迁移 SQL |
src/main/java/com/simon/DocMind/entity/SysRole.java | 新增 |
src/main/java/com/simon/DocMind/entity/SysPermission.java | 新增 |
src/main/java/com/simon/DocMind/entity/SysUserRole.java | 新增 |
src/main/java/com/simon/DocMind/entity/SysRolePermission.java | 新增 |
src/main/java/com/simon/DocMind/service/PermissionService.java | 新增 |
src/main/java/com/simon/DocMind/service/RoleService.java | 新增 |
src/main/java/com/simon/DocMind/common/utils/JwtUtils.java | 改 claim 结构 |
src/main/java/com/simon/DocMind/security/UserDetailsServiceImpl.java | 改 authority 来源 |
src/main/java/com/simon/DocMind/security/JwtAuthenticationFilter.java | 增加 revoke 时间戳校验 |
src/main/java/com/simon/DocMind/config/SecurityConfig.java | 移除 URL 级 hasRole |
src/main/java/com/simon/DocMind/common/constant/Perms.java | 新增(权限码常量) |
src/main/java/com/simon/DocMind/aop/OwnerCheckAspect.java | 新增 |
src/main/java/com/simon/DocMind/aop/AuditLogAspect.java | 新增 |
| 7 个 Controller | @PreAuthorize 重写 |
DocMind-frontend/src/stores/user.ts | 增 permissions + hasPerm |
DocMind-frontend/src/router/index.ts | 守卫补 requiresPerm |
DocMind-frontend/src/directives/perm.ts | 新增 v-perm 指令 |
DocMind-frontend/src/views/admin/role/ | 新增角色管理页 |
6. 平滑迁移策略
- 双写期(Phase 1 上线后 1 周):保留
sys_user.role字段,每次写用户角色时同步写入旧字段,便于回滚 - JWT 兼容:JwtAuthenticationFilter 同时识别旧
roleclaim 和新permsclaim,老 token 自然过期失效(24h) - 权限码缓存预热:服务启动时把所有用户权限码批量加载到 Redis
- 回滚预案:保留旧
SecurityConfig在legacy分支,发现严重问题可一键切回
7. 验收测试
| 场景 | 预期 |
|---|---|
| 普通用户上传文档 | 403 kb:upload 拒绝 |
普通用户访问 /api/admin/ai-config | 403 ai-config:read 拒绝 |
| KB 管理员看不到用户列表 | 403 user:read 拒绝 |
| 普通用户 A 删除用户 B 的知识库 | 403(OwnerCheck 拦截) |
给某用户加 kb:upload 权限后 5 分钟内生效 | Redis revoke 触发 token 重签 |
| 角色 CRUD | sys_audit_log 留痕 |
| 老 token(无 perms claim)过期前可继续用 | 兼容期通过 |
测试入口:
- 单元:
mvn test -Dtest=PermissionServiceTest - 集成:
mvn test -Dtest=RbacIntegrationTest(用 Testcontainers 拉 MySQL + Redis) - E2E:登录三类角色账号,跑 Postman collection
docs/rbac-postman.json
8. 风险与权衡
| 风险 | 缓解 |
|---|---|
| 权限码列表过大(用户挂多个高权角色 → JWT 体积膨胀) | 单用户权限超过 50 个时,JWT 只放角色码,鉴权时查 Redis(用 token bytes 监控触发降级) |
| 权限热更新引入 Redis 强依赖 | Redis 不可用时,降级为"必须等 token 自然过期才生效",业务可用性不受影响 |
| 注解散落仍有遗漏 | 用 ArchUnit 测试强制 controller 包内每个 public 方法必须有 @PreAuthorize 或 @PermitAll |
| Owner 检查切面性能 | 单接口 +1 次主键查询,可接受;高频接口(如 /chat/stream)改为查询时 SQL 内 WHERE user_id=? 而非切面 |
9. 面试讲故事的角度
- 为什么不用 Spring Security ACL? —— ACL 适合对象级 ACE 矩阵(每个文档对每个用户单独授权),DocMind 场景是"按角色批量授权 + owner 隔离",三段式 RBAC 已够,ACL 反而复杂
- 为什么 JWT 里塞权限而不是只塞角色? —— 权限放 token:鉴权零查询,性能最好;代价是"权限变更延迟"(用 revoke 戳兜底)。塞角色则每次请求都要查"角色→权限"映射,大流量场景吃不消
- 数据级权限为什么用切面而不是 SQL 拦截器? —— 切面侵入低、可读,覆盖增删改场景;列表查询用 MyBatis 拦截器统一加
user_id过滤更合适,两者结合 - 如果再加多租户怎么办? ——
sys_role增tenant_id,sys_permission保持全局,sys_user_role增租户上下文,Owner 切面升级为 Tenant + Owner 双校验
10. 同步面试文档
完成实施后需更新:
- 01-项目全景与架构设计.md — 安全模块章节
- 05-工程化实践.md — 新增"权限系统"小节
- 13-简历素材与STAR故事.md — 新增 STAR:从单 role 升级到多角色细粒度 RBAC,影响面 = 7 个 controller / 30+ 接口