Skip to content

📊 数据权限

1. 概述

数据权限是指根据用户角色和权限范围,控制用户能够访问的数据范围。ZsAdmin实现了基于机构的细粒度数据权限控制,确保用户只能访问其权限范围内的数据,有效保护数据安全和隐私。

2. 数据权限模型

2.1 权限范围

ZsAdmin支持以下5种数据权限范围,满足不同业务场景需求:

权限范围描述适用场景
🔄 全部数据可以访问系统中所有数据系统管理员、超级管理员
🎯 自定义可以访问指定机构的数据跨部门管理者、数据分析师
🏢 所在机构只能访问当前机构的数据部门负责人、普通管理员
🌳 所在机构及以下可以访问当前机构及其所有层级的子机构数据上级部门负责人、区域管理者
👤 仅自己只能访问自己创建的数据普通员工、一线操作人员

2.2 实现原理

数据权限通过MyBatis Plus的插件机制实现,在SQL执行前动态解析并添加数据范围条件,核心优势:

  • ✅ 无侵入性设计,无需修改现有SQL
  • 🔧 支持动态扩展,可自定义数据权限规则
  • ⚡ 高性能实现,SQL级别的权限控制
  • 🔍 细粒度控制,支持多维度权限组合

3. 核心实现

3.1 数据权限插件配置

java
@Configuration
@EnableConfigurationProperties(TenantProperties.class)
public class MybatisPlusConfig {

    @NotNull
    @Bean
    public MybatisPlusInterceptor mybatisPlusInterceptor(TenantProperties tenantProperties) {
        MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
        
        // 多租户插件(可选)
        if (Boolean.TRUE.equals(tenantProperties.getEnable())) {
            interceptor.addInnerInterceptor(new TenantLineInnerInterceptor(new MultiTenantHandler(tenantProperties)));
        }

        // 乐观锁插件
        interceptor.addInnerInterceptor(new OptimisticLockerInnerInterceptor());

        // 防止全表更新和删除
        interceptor.addInnerInterceptor(new BlockAttackInnerInterceptor());

        // 🎯 数据权限插件 - 核心配置
        MyDataPermissionInterceptor myDataPermissionInterceptor = new MyDataPermissionInterceptor();
        myDataPermissionInterceptor.setDataPermissionHandler(new MyDataPermissionHandler());
        interceptor.addInnerInterceptor(myDataPermissionInterceptor);

        // 分页插件
        interceptor.addInnerInterceptor(new PaginationInnerInterceptor());

        return interceptor;
    }
}

3.2 数据权限处理器

java
/**
 * 🎯 数据权限处理器
 * 核心逻辑:根据用户权限动态生成SQL过滤条件
 */
@Component
public class MyDataPermissionHandler {

    private static final Logger logger = LoggerFactory.getLogger(MyDataPermissionHandler.class);

    /**
     * 生成数据权限SQL片段
     */
    @Nullable
    public Expression getSqlSegment(@NotNull PlainSelect plainSelect, @NotNull String mappedStatementId) {
        // 1. 获取方法上的@DataScope注解
        DataScope dataScope = this.getDataScope(mappedStatementId);
        if (dataScope == null) {
            return plainSelect.getWhere(); // 无注解,不进行数据权限过滤
        }

        // 2. 执行数据权限过滤
        return doFilter(plainSelect, plainSelect.getWhere(), dataScope);
    }

    /**
     * 核心过滤逻辑
     */
    @Nullable
    public Expression doFilter(@NotNull PlainSelect plainSelect, @Nullable Expression where, @NotNull DataScope dataScope) {
        // 🔐 安全校验:获取当前登录用户信息
        LoginUserInfo loginUserInfo = SecurityUtil.getUserInfo();
        if (loginUserInfo == null || loginUserInfo.getSysUser() == null) {
            logger.warn("未获取到登录用户信息,跳过数据权限过滤");
            return where;
        }

        // 🔐 权限校验:管理员直接跳过数据权限过滤
        if (Objects.equals(loginUserInfo.getSysUser().getIsAdmin(), AdminEnum.Admin.getValue())) {
            return where;
        }

        // 🔐 权限校验:获取用户数据权限范围
        Set<DataScopeEnum> dataScopeTypes = Optional.ofNullable(loginUserInfo.getDataPermission())
                .map(DataPermission::getDataScopeTypes)
                .orElse(Collections.emptySet());
        if (dataScopeTypes.isEmpty() || dataScopeTypes.contains(DataScopeEnum.ALL)) {
            return where; // 无权限或全部权限,直接返回
        }

        // 📊 解析表信息和字段配置
        Table table = (Table) plainSelect.getFromItem();
        String aliasName = getTableAlias(table, dataScope);
        String deptColumn = Optional.ofNullable(dataScope.deptField()).orElse(aliasName + ".create_dept_id");
        String userColumn = Optional.ofNullable(dataScope.userField()).orElse(aliasName + ".creator");

        // 🎯 生成数据权限条件表达式
        List<Expression> expressions = dataScopeTypes.stream()
                .filter(scope -> scope != DataScopeEnum.ALL)
                .map(scope -> createExpressionForScope(scope, loginUserInfo, deptColumn, userColumn))
                .filter(Objects::nonNull)
                .toList();

        // 🔗 组合多条件为OR关系
        Expression dataPermissionExpression = expressions.stream()
                .reduce(OrExpression::new)
                .orElse(null);

        // 🛡️ 安全处理:无有效权限条件时返回原始WHERE
        if (dataPermissionExpression == null) {
            return where;
        }
        
        // 📝 组合原始条件和数据权限条件
        return where == null 
                ? dataPermissionExpression 
                : new AndExpression(where, new Parenthesis(dataPermissionExpression));
    }

    /**
     * 获取表别名(优先使用注解配置,其次使用SQL表别名,最后使用表名)
     */
    private String getTableAlias(Table table, DataScope dataScope) {
        String annotationAlias = dataScope.tableAlias();
        if (annotationAlias != null && !annotationAlias.isEmpty()) {
            return annotationAlias;
        }
        return Optional.ofNullable(table.getAlias())
                .map(Alias::getName)
                .orElse(table.getName());
    }

    /**
     * 根据数据范围类型创建对应的SQL表达式
     * 使用Java 14 switch表达式,简洁高效
     */
    private Expression createExpressionForScope(DataScopeEnum dataScope, LoginUserInfo loginUserInfo,
                                               String deptColumn, String userColumn) {
        return switch (dataScope) {
            case CUSTOM -> createInExpression(deptColumn, loginUserInfo.getDataPermission().getDeptIds());
            case DEPT -> createEqualsExpression(deptColumn, loginUserInfo.getSysUser().getSysDeptId());
            case DEPT_AND_CHILD -> createInExpression(deptColumn, loginUserInfo.getDataPermission().getDeptIds());
            case SELF -> createEqualsExpression(userColumn, loginUserInfo.getSysUser().getSysUserId());
            default -> null;
        };
    }

    /**
     * 创建IN条件表达式(复用方法,减少重复代码)
     */
    private Expression createInExpression(String columnName, Set<Long> values) {
        if (values.isEmpty()) return null;
        List<Expression> valueExpressions = values.stream()
                .map(LongValue::new)
                .map(longValue -> (Expression) longValue)
                .collect(Collectors.toList());
        return new InExpression(new Column(columnName), 
                new Parenthesis(new ExpressionList<>(valueExpressions)));
    }

    /**
     * 创建=条件表达式(复用方法,减少重复代码)
     */
    private Expression createEqualsExpression(String columnName, Long value) {
        return new EqualsTo(new Column(columnName), new LongValue(value));
    }

    /**
     * 从Mapper方法中获取@DataScope注解
     */
    @Nullable
    private DataScope getDataScope(@NotNull String mappedStatementId) {
        try {
            String className = mappedStatementId.substring(0, mappedStatementId.lastIndexOf("."));
            String methodName = mappedStatementId.substring(mappedStatementId.lastIndexOf(".") + 1);
            
            Class<?> clazz = Class.forName(className);
            return Arrays.stream(clazz.getDeclaredMethods())
                    .filter(m -> m.getName().equals(methodName))
                    .findFirst()
                    .map(method -> method.getAnnotation(DataScope.class))
                    .orElse(null);
        } catch (ClassNotFoundException e) {
            logger.error("获取mapper类失败", e);
            return null;
        }
    }
}

3.3 数据权限注解

java
/**
 * 🎯 数据权限注解
 * 用于标记需要进行数据权限过滤的Mapper方法
 */
@Target({ElementType.PARAMETER, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface DataScope {

    /**
     * 表别名(多表关联时使用)
     */
    String tableAlias() default "";

    /**
     * 部门限制范围的字段名称
     * 默认:creator_dept(创建人部门ID)
     */
    String deptField() default "creator_dept";

    /**
     * 用户限制范围的字段名称
     * 默认:creator(创建人ID)
     */
    String userField() default "creator";

}

4. 数据权限配置

4.1 角色数据权限配置

在角色管理模块中,可以为每个角色配置数据权限范围:

  1. 选择权限范围:从5种权限范围中选择合适的权限
  2. 自定义权限配置
    • 选择具体的机构/部门
    • 支持多选和层级选择
    • 可视化机构树操作
  3. 权限继承:子角色默认继承父角色的数据权限

5. 数据权限使用

5.1 在Mapper层使用

使用方式简单快捷:只需在Mapper方法上添加@DataScope注解即可

java
@Mapper
public interface SysUserMapper extends DataPermissionMapper<SysUserEntity> {

    /**
     * 🎯 查询用户列表(带数据权限)
     */
    @DataScope(tableAlias = "u", deptField = "u.dept_id", userField = "u.create_by")
    IPage<SysUserEntity> page(Page<SysUserEntity> page, @Param("params") Map<String, Object> params);

    /**
     * 🎯 查询用户详情(带数据权限)
     */
    @DataScope
    SysUserEntity selectUserById(@Param("id") Long id);

}

5.2 在XML中使用

无需修改XML,数据权限插件会自动处理

xml
<select id="selectUserList" resultType="SysUser">
    select * from sys_user u
    <where>
        <!-- 其他业务条件 -->
        <if test="username != null and username != ''">
            and u.username like concat('%', #{username}, '%')
        </if>
        <!-- 🎯 数据权限条件会自动添加到这里 -->
    </where>
</select>

6. 应用场景

6.1 企业管理系统

用户角色数据权限配置业务效果
👑 CEO🔄 全部数据查看全公司所有数据,掌握全局运营情况
🧑💼 部门经理🌳 所在机构及以下查看本部门及所有子部门数据,管理团队绩效
👨💼 主管🏢 所在机构查看本部门数据,管理部门日常事务
👩💻 普通员工👤 仅自己只能查看自己创建的数据,保护个人工作空间

6.2 多租户系统

  • 租户隔离:每个租户只能访问自己的数据
  • 跨租户管理:支持平台管理员管理所有租户数据
  • 租户内权限:租户内可按角色配置不同数据权限
  • 数据共享:支持租户间数据授权和共享

6.3 数据敏感场景

  • 财务数据:仅财务人员可查看,严格的数据权限控制
  • 客户信息:仅销售团队可查看,保护客户隐私
  • 操作日志:仅管理员可查看,确保系统安全
  • 统计报表:按角色配置不同的数据可见范围

7. 最佳实践

  1. 最小权限原则:仅授予用户必要的数据访问权限
  2. 权限继承设计:合理设计角色层级,避免权限冲突
  3. 定期权限审计:定期检查和调整用户数据权限
  4. 数据脱敏配合:敏感数据需结合数据脱敏技术
  5. 日志记录:记录所有数据访问行为,便于审计和追溯
  6. 性能优化
    • 合理设计数据权限规则,避免复杂SQL
    • 对频繁访问的数据进行缓存
    • 定期优化数据权限查询性能

8. 常见问题

Q: 数据权限不生效怎么办?

A: 检查以下几点:

  • ✅ 确认已添加@DataScope注解
  • ✅ 确认插件配置正确
  • ✅ 确认用户已分配数据权限
  • ✅ 查看日志,检查是否有错误信息

Q: 多表关联时如何使用数据权限?

A: 使用tableAlias属性指定表别名:

java
@DataScope(tableAlias = "u", deptField = "u.dept_id")

Q: 如何自定义数据权限规则?

A: 继承DataPermissionHandler类,重写相应方法,实现自定义逻辑

9. 总结

ZsAdmin的数据权限系统采用了插件化、注解化、动态化的设计理念,实现了高效、灵活、细粒度的数据权限控制。通过合理配置和使用数据权限,可以有效保护数据安全,防止数据泄露,同时提高系统的可维护性和扩展性。

数据权限是企业级应用的重要组成部分,在设计和实现时需要综合考虑业务需求、性能要求和安全标准,确保系统既安全又易用。