# DataMate 用户权限体系完整分析报告 ## 1. 数据库层面 ### 1.1 需要新增的表 #### RBAC 核心表 ```sql -- 用户表(已有 users,需扩展) ALTER TABLE users ADD COLUMN id VARCHAR(36) PRIMARY KEY; ALTER TABLE users ADD COLUMN status TINYINT DEFAULT 1; ALTER TABLE users ADD COLUMN created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP; -- 角色表 CREATE TABLE t_sys_roles ( id VARCHAR(36) PRIMARY KEY, code VARCHAR(50) NOT NULL UNIQUE, name VARCHAR(100) NOT NULL, description VARCHAR(500), is_system BOOLEAN DEFAULT FALSE, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ); -- 权限表 CREATE TABLE t_sys_permissions ( id VARCHAR(36) PRIMARY KEY, code VARCHAR(100) NOT NULL UNIQUE, name VARCHAR(100) NOT NULL, resource_type VARCHAR(50), -- MENU/API/DATA resource_path VARCHAR(200), action VARCHAR(20), -- READ/WRITE/DELETE/EXECUTE description VARCHAR(500), created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ); -- 用户角色关联表 CREATE TABLE t_sys_user_roles ( id VARCHAR(36) PRIMARY KEY, user_id VARCHAR(36) NOT NULL, role_id VARCHAR(36) NOT NULL, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, UNIQUE KEY uk_user_role (user_id, role_id) ); -- 角色权限关联表 CREATE TABLE t_sys_role_permissions ( id VARCHAR(36) PRIMARY KEY, role_id VARCHAR(36) NOT NULL, permission_id VARCHAR(36) NOT NULL, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, UNIQUE KEY uk_role_permission (role_id, permission_id) ); ``` ### 1.2 需要修改的现有表 ```sql -- 数据集表 ALTER TABLE t_dm_datasets ADD COLUMN created_by VARCHAR(36); ALTER TABLE t_dm_datasets ADD COLUMN updated_by VARCHAR(36); ALTER TABLE t_dm_datasets ADD COLUMN owner_id VARCHAR(36); ALTER TABLE t_dm_datasets ADD COLUMN tenant_id VARCHAR(36); ALTER TABLE t_dm_datasets ADD COLUMN is_public BOOLEAN DEFAULT FALSE; -- 标注模板表 ALTER TABLE t_dm_annotation_templates ADD COLUMN created_by VARCHAR(36); ALTER TABLE t_dm_annotation_templates ADD COLUMN updated_by VARCHAR(36); -- 其他核心表(标注任务、操作符等)都需要添加类似字段 ``` ### 1.3 RBAC 模型设计 **基于角色的访问控制(RBAC)架构**: - 用户 → 用户角色关联 → 角色 - 角色 → 角色权限关联 → 权限 - 权限 = 资源类型 + 资源路径 + 操作 **权限编码规则**: - `DATASET:READ` - 数据集读取 - `DATASET:WRITE` - 数据集写入 - `DATASET:DELETE` - 数据集删除 - `DATASET:SHARE` - 数据集共享 - `ANNOTATION:CREATE` - 创建标注任务 - `ANNOTATION:READ` - 读取标注结果 - `ANNOTATION:WRITE` - 修改标注 - `ANNOTATION:DELETE` - 删除标注 --- ## 2. 后端层面 ### 2.1 Spring Boot 依赖和配置 **需要添加的依赖**: ```xml org.springframework.boot spring-boot-starter-security io.jsonwebtoken jjwt-api 0.11.5 io.jsonwebtoken jjwt-impl 0.11.5 runtime io.jsonwebtoken jjwt-jackson 0.11.5 ``` ### 2.2 需要创建的新模块 #### 建议目录结构: ``` backend/shared/ domain-common/ # 领域公共模块 - src/main/java/com/datamate/common/domain/ - entity/Role.java - entity/Permission.java - entity/UserRole.java - entity/RolePermission.java - repository/RoleRepository.java - repository/PermissionRepository.java security-common/ # 已存在,扩展 - src/main/java/com/datamate/common/security/ - JwtUtils.java (已有) - JwtAuthenticationFilter.java (新增) - SecurityConfig.java (新增) - UserDetailsServiceImpl.java (新增) - CustomUserDetailsService.java (新增) backend/services/ auth-service/ # 认证服务(可选) - src/main/java/com/datamate/auth/ - controller/AuthController.java - service/AuthService.java - dto/LoginRequest.java - dto/LoginResponse.java ``` ### 2.3 关键 Service、Controller、Repository #### 认证服务 ```java // backend/shared/security-common/src/main/java/com/datamate/common/security/JwtAuthenticationFilter.java @Component public class JwtAuthenticationFilter extends OncePerRequestFilter { @Autowired private JwtUtils jwtUtils; @Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) { String token = getTokenFromRequest(request); if (token != null && jwtUtils.validateToken(token)) { String username = jwtUtils.getUsernameFromToken(token); UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(username, null, getAuthorities(token)); SecurityContextHolder.getContext().setAuthentication(authentication); } filterChain.doFilter(request, response); } } // backend/shared/security-common/src/main/java/com/datamate/common/security/SecurityConfig.java @Configuration @EnableWebSecurity @EnableMethodSecurity(prePostEnabled = true) public class SecurityConfig { @Bean public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { http.csrf(csrf -> csrf.disable()) .sessionManagement(session -> session .sessionCreationPolicy(SessionCreationPolicy.STATELESS)) .authorizeHttpRequests(auth -> auth .requestMatchers("/auth/**").permitAll() .requestMatchers("/public/**").permitAll() .anyRequest().authenticated()) .addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class); return http.build(); } } ``` #### 权限服务 ```java // backend/shared/domain-common/src/main/java/com/datamate/common/domain/service/PermissionService.java @Service public class PermissionService { @Autowired private RoleRepository roleRepository; public Set getPermissionsByUserId(String userId) { return roleRepository.findPermissionsByUserId(userId) .stream() .map(Permission::getCode) .collect(Collectors.toSet()); } } ``` ### 2.4 权限拦截器和注解 #### @PreAuthorize 使用示例 ```java // backend/services/data-management-service/src/main/java/.../DatasetController.java @RestController @RequestMapping("/api/datasets") public class DatasetController { @GetMapping @PreAuthorize("hasAuthority('DATASET:READ')") public List getDatasets() { return datasetService.getDatasets(); } @PostMapping @PreAuthorize("hasAuthority('DATASET:WRITE')") public Dataset createDataset(@RequestBody Dataset dataset) { dataset.setOwner(getCurrentUserId()); return datasetService.create(dataset); } @DeleteMapping("/{id}") @PreAuthorize("hasAuthority('DATASET:DELETE') or @datasetService.isOwner(#id, authentication.name)") public void deleteDataset(@PathVariable String id) { datasetService.delete(id); } } ``` #### 自定义权限注解(可选) ```java @Target({ElementType.METHOD, ElementType.TYPE}) @Retention(RetentionPolicy.RUNTIME) @PreAuthorize("@permissionService.hasPermission(authentication, #resourceType, #action)") public @interface RequirePermission { String resourceType(); String action(); } // 使用示例 @RequirePermission(resourceType = "DATASET", action = "READ") public List getDatasets() { ... } ``` --- ## 3. 前端层面 ### 3.1 权限存储和传递 #### Redux Store 扩展 ```typescript // frontend/src/store/authSlice.ts export interface AuthState { isAuthenticated: boolean; token: string | null; user: User | null; permissions: string[]; // 新增 } const authSlice = createSlice({ name: 'auth', initialState, reducers: { loginSuccess: (state, action) => { state.token = action.payload.token; state.user = action.payload.user; state.permissions = action.payload.permissions; // 新增 state.isAuthenticated = true; }, logout: (state) => { state.token = null; state.user = null; state.permissions = []; state.isAuthenticated = false; }, }, }); ``` ### 3.2 基于权限的 UI 显示/隐藏 #### 权限检查 Hook ```typescript // frontend/src/hooks/usePermission.ts import { useSelector } from 'react-redux'; import { RootState } from '../store'; export const usePermission = () => { const permissions = useSelector((state: RootState) => state.auth.permissions); const hasPermission = (required: string | string[]): boolean => { const requiredPerms = Array.isArray(required) ? required : [required]; return requiredPerms.every(p => permissions.includes(p)); }; return { hasPermission }; }; // 使用示例 const { hasPermission } = usePermission(); {hasPermission('DATASET:WRITE') && ( )} ``` #### 高阶组件包装 ```typescript // frontend/src/components/PermissionWrapper.tsx interface PermissionWrapperProps { permission: string | string[]; children: React.ReactNode; fallback?: React.ReactNode; } export const PermissionWrapper: React.FC = ({ permission, children, fallback = null }) => { const { hasPermission } = usePermission(); return hasPermission(permission) ? <>{children} : <>{fallback}; }; // 使用示例 ``` ### 3.3 路由守卫和权限校验 #### 受保护路由 ```typescript // frontend/src/components/ProtectedRoute.tsx import { Navigate, Outlet } from 'react-router-dom'; import { usePermission } from '../hooks/usePermission'; interface ProtectedRouteProps { required?: string[]; } export const ProtectedRoute: React.FC = ({ required = [] }) => { const isAuthenticated = useSelector((s: RootState) => s.auth.isAuthenticated); const { hasPermission } = usePermission(); if (!isAuthenticated) return ; if (!hasPermission(required)) return ; return ; }; // 使用示例 }> } /> ``` ### 3.4 需要修改的页面和组件 **需要修改的页面**: - 菜单(Sidebar/Navbar):基于权限过滤菜单项 - 操作按钮:创建、编辑、删除按钮根据权限显示/隐藏 - 页面入口:Data Management、Annotation、Operator Market 等 **示例代码**: ```typescript // frontend/src/components/Sidebar/menu.tsx const menuItems = [ { path: '/data/management', icon: , label: '数据管理', required: ['DATASET:READ'], // 权限要求 }, { path: '/annotation', icon: , label: '数据标注', required: ['ANNOTATION:READ'], }, ].filter(item => hasPermission(item.required || [])); ``` --- ## 4. 现有问题和隐患 ### 4.1 无权限控制的地方 **严重问题**: - `SecurityConfig` 当前为 `permitAll()`,所有 API 对外裸露 - `application.yml` 排除了 Spring Security 自动配置 - 没有任何 @PreAuthorize 或权限检查 **前端问题**: - `authSlice` 中的 `loginLocal` 直接写入 mock token - 没有真实的登录 API 调用 - 权限信息未从后端获取 ### 4.2 数据迁移策略 #### 迁移脚本示例 ```sql -- 添加 owner_id(如果没有指定,默认为系统用户) UPDATE t_dm_datasets SET owner_id = '00000000-0000-0000-0000-000000000000' WHERE owner_id IS NULL; -- 添加审计字段 ALTER TABLE t_dm_datasets ADD COLUMN created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP; ALTER TABLE t_dm_datasets ADD COLUMN updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP; ``` ### 4.3 数据隔离问题 **当前问题**: - `DatasetRepositoryImpl` 等仓库层查询无 owner/tenant 过滤 - 用户可以看到所有用户创建的数据集 - 标注任务也存在同样问题 **解决方案**: ```java // MyBatis 拦截器 @Intercepts({@Signature(type= Executor.class, method="update", args={MappedStatement.class, Object.class})}) @Component public class DataScopeInterceptor implements Interceptor { @Override public Object intercept(Invocation invocation) { MappedStatement ms = (MappedStatement) invocation.getArgs()[0]; Object parameter = invocation.getArgs()[1]; String userId = SecurityContextHolder.getContext() .getAuthentication().getName(); // 自动添加 WHERE owner_id = ? 条件 // 实现 SQL 改写或参数注入 return invocation.proceed(); } } ``` ### 4.4 可能的安全漏洞 **高危漏洞**: 1. **JWT 默认 secret**:`JwtUtils` 中默认使用 `datamate-secret-key...` - 建议:必须通过环境变量配置 2. **Token 存储在 localStorage**:易受 XSS 攻击 - 建议:使用 HTTP-only cookie + CSRF token 3. **无 token 过期处理**:token 永不过期 - 建议:设置合理过期时间(如 7 天) 4. **审计字段不可信**:`EntityMetaObjectHandler` 默认返回 `system` - 建议:从 SecurityContext 获取当前用户 --- ## 5. 实施建议 ### 5.1 优先级和实施顺序 **Phase 1:基础架构(1-2 周)** 1. 创建 RBAC 数据库表 2. 扩展 users 表和现有核心表 3. 建立基础账号和管理员角色 **Phase 2:认证授权(2-3 周)** 1. 搭建 Auth Service 或在 main-application 增加 `/auth` 模块 2. 实现 JWT 生成和验证 3. 实现登录/刷新 token 接口 **Phase 3:后端集成(2-3 周)** 1. 在各业务服务启用 Spring Security 2. 添加 @PreAuthorize 注解 3. 实现数据隔离(仓库层过滤) **Phase 4:前端集成(2-3 周)** 1. 替换 mock 登录,调用 `/auth/me` 2. 实现权限路由守卫 3. 实现基于权限的 UI 控制 **Phase 5:全面测试(1-2 周)** 1. 单元测试、集成测试 2. 回归测试 3. 安全测试 ### 5.2 向后兼容性考虑 **开发模式**: - 通过 `security.enabled=false` 保留 `permitAll` 模式 - 允许本地开发时快速迭代 **生产模式**: - 强制启用认证 - 所有 API 必须有 token **灰度发布**: - 可以先在特定用户组启用 - 逐步扩大范围 ### 5.3 测试策略 **单元测试**: - 权限判定函数 - Role→Permission 解析 - JWT 生成和验证 **集成测试**: - 登录 → 获取 token - API 权限拒绝/允许测试 **回归测试**: - 数据集/任务等列表是否被正确过滤 **安全测试**: - 401 未认证 - 403 无权限 - 越权访问 - Token 过期 - 权限变更后的即时失效