feat(user-permission): 实现用户权限体系并修复所有问题

- 数据库层面:
  - 创建 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 刷新逻辑)
This commit is contained in:
2026-02-04 04:33:13 +00:00
parent 340a403c54
commit 7606cd34bd
22 changed files with 4840 additions and 0 deletions

View File

@@ -0,0 +1,533 @@
# 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 过期
- 权限变更后的即时失效