You've already forked DataMate
- 数据库层面: - 创建 RBAC 核心表(角色、菜单权限) - 扩展现有表支持数据共享 - 初始化基础数据 - 后端层面: - 实现 UserContext 用户上下文管理 - 实现数据集访问权限服务 - 实现菜单权限服务 - 添加数据集共享功能 - 修复前端命名不匹配问题(snake_case vs camelCase) - 修复请求头不匹配问题(X-User-Roles vs X-Role-Codes) - 修复 Mapper 方法未实现问题 - 修复共享设置持久化缺失问题 - 前端层面: - 创建菜单权限工具 - 更新 Redux Store 支持菜单过滤 - 创建数据集共享设置组件 - 添加用户信息到请求头 - 实现 Token 刷新逻辑 - 数据隔离: - 实现 MyBatis 查询权限检查 - 实现数据文件访问控制 参考: - Codex 生成的实施方案 - kimI-cli 实施结果 - Codex Review 审核报告 修复的问题: 1. 前端命名不匹配(is_shared -> isShared, shared_with -> sharedWith) 2. 请求头不匹配(X-User-Roles -> X-Role-Codes) 3. Mapper 方法未实现(添加 findFilesWithAccessCheck 等方法声明) 4. 共享设置持久化缺失(添加 isShared 和 sharedWith 字段到 UpdateDatasetRequest) 5. 用户上下文加载问题(实现 Token 刷新逻辑)
15 KiB
15 KiB
DataMate 用户权限体系完整分析报告
1. 数据库层面
1.1 需要新增的表
RBAC 核心表
-- 用户表(已有 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 需要修改的现有表
-- 数据集表
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 依赖和配置
需要添加的依赖:
<!-- backend/shared/security-common/pom.xml -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-api</artifactId>
<version>0.11.5</version>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-impl</artifactId>
<version>0.11.5</version>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-jackson</artifactId>
<version>0.11.5</version>
</dependency>
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
认证服务
// 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();
}
}
权限服务
// backend/shared/domain-common/src/main/java/com/datamate/common/domain/service/PermissionService.java
@Service
public class PermissionService {
@Autowired
private RoleRepository roleRepository;
public Set<String> getPermissionsByUserId(String userId) {
return roleRepository.findPermissionsByUserId(userId)
.stream()
.map(Permission::getCode)
.collect(Collectors.toSet());
}
}
2.4 权限拦截器和注解
@PreAuthorize 使用示例
// backend/services/data-management-service/src/main/java/.../DatasetController.java
@RestController
@RequestMapping("/api/datasets")
public class DatasetController {
@GetMapping
@PreAuthorize("hasAuthority('DATASET:READ')")
public List<Dataset> 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);
}
}
自定义权限注解(可选)
@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<Dataset> getDatasets() { ... }
3. 前端层面
3.1 权限存储和传递
Redux Store 扩展
// 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
// 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') && (
<Button type="primary">创建数据集</Button>
)}
高阶组件包装
// frontend/src/components/PermissionWrapper.tsx
interface PermissionWrapperProps {
permission: string | string[];
children: React.ReactNode;
fallback?: React.ReactNode;
}
export const PermissionWrapper: React.FC<PermissionWrapperProps> =
({ permission, children, fallback = null }) => {
const { hasPermission } = usePermission();
return hasPermission(permission) ? <>{children}</> : <>{fallback}</>;
};
// 使用示例
<PermissionWrapper permission="DATASET:DELETE">
<Button danger>删除数据集</Button>
</PermissionWrapper>
3.3 路由守卫和权限校验
受保护路由
// 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<ProtectedRouteProps> = ({ required = [] }) => {
const isAuthenticated = useSelector((s: RootState) => s.auth.isAuthenticated);
const { hasPermission } = usePermission();
if (!isAuthenticated) return <Navigate to="/login" replace />;
if (!hasPermission(required)) return <Navigate to="/403" replace />;
return <Outlet />;
};
// 使用示例
<Route element={<ProtectedRoute required={['DATASET:READ']} />}>
<Route path="/data/management" element={<DatasetManagement />} />
</Route>
3.4 需要修改的页面和组件
需要修改的页面:
- 菜单(Sidebar/Navbar):基于权限过滤菜单项
- 操作按钮:创建、编辑、删除按钮根据权限显示/隐藏
- 页面入口:Data Management、Annotation、Operator Market 等
示例代码:
// frontend/src/components/Sidebar/menu.tsx
const menuItems = [
{
path: '/data/management',
icon: <Database />,
label: '数据管理',
required: ['DATASET:READ'], // 权限要求
},
{
path: '/annotation',
icon: <PenTool />,
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 数据迁移策略
迁移脚本示例
-- 添加 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 过滤- 用户可以看到所有用户创建的数据集
- 标注任务也存在同样问题
解决方案:
// 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 可能的安全漏洞
高危漏洞:
-
JWT 默认 secret:
JwtUtils中默认使用datamate-secret-key...- 建议:必须通过环境变量配置
-
Token 存储在 localStorage:易受 XSS 攻击
- 建议:使用 HTTP-only cookie + CSRF token
-
无 token 过期处理:token 永不过期
- 建议:设置合理过期时间(如 7 天)
-
审计字段不可信:
EntityMetaObjectHandler默认返回system- 建议:从 SecurityContext 获取当前用户
5. 实施建议
5.1 优先级和实施顺序
Phase 1:基础架构(1-2 周)
- 创建 RBAC 数据库表
- 扩展 users 表和现有核心表
- 建立基础账号和管理员角色
Phase 2:认证授权(2-3 周)
- 搭建 Auth Service 或在 main-application 增加
/auth模块 - 实现 JWT 生成和验证
- 实现登录/刷新 token 接口
Phase 3:后端集成(2-3 周)
- 在各业务服务启用 Spring Security
- 添加 @PreAuthorize 注解
- 实现数据隔离(仓库层过滤)
Phase 4:前端集成(2-3 周)
- 替换 mock 登录,调用
/auth/me - 实现权限路由守卫
- 实现基于权限的 UI 控制
Phase 5:全面测试(1-2 周)
- 单元测试、集成测试
- 回归测试
- 安全测试
5.2 向后兼容性考虑
开发模式:
- 通过
security.enabled=false保留permitAll模式 - 允许本地开发时快速迭代
生产模式:
- 强制启用认证
- 所有 API 必须有 token
灰度发布:
- 可以先在特定用户组启用
- 逐步扩大范围
5.3 测试策略
单元测试:
- 权限判定函数
- Role→Permission 解析
- JWT 生成和验证
集成测试:
- 登录 → 获取 token
- API 权限拒绝/允许测试
回归测试:
- 数据集/任务等列表是否被正确过滤
安全测试:
- 401 未认证
- 403 无权限
- 越权访问
- Token 过期
- 权限变更后的即时失效