Files
DataMate/permission-analysis-report.md
Jerry Yan 7606cd34bd 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 刷新逻辑)
2026-02-04 04:33:13 +00:00

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 可能的安全漏洞

高危漏洞

  1. JWT 默认 secretJwtUtils 中默认使用 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 过期
  • 权限变更后的即时失效