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 刷新逻辑)
534 lines
15 KiB
Markdown
534 lines
15 KiB
Markdown
# 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
|
|
<!-- 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
|
|
|
|
#### 认证服务
|
|
```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<String> 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<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);
|
|
}
|
|
}
|
|
```
|
|
|
|
#### 自定义权限注解(可选)
|
|
```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<Dataset> 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') && (
|
|
<Button type="primary">创建数据集</Button>
|
|
)}
|
|
```
|
|
|
|
#### 高阶组件包装
|
|
```typescript
|
|
// 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 路由守卫和权限校验
|
|
|
|
#### 受保护路由
|
|
```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<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 等
|
|
|
|
**示例代码**:
|
|
```typescript
|
|
// 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 数据迁移策略
|
|
|
|
#### 迁移脚本示例
|
|
```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 过期
|
|
- 权限变更后的即时失效
|