Java案例如何实现数据权限?

wen python案例 2

本文目录导读:

Java案例如何实现数据权限?

  1. 核心思路
  2. 常见实现方案
  3. 缓存优化建议
  4. 最佳实践建议
  5. 注意事项

在Java应用中实现数据权限(也称为行级权限或数据级权限)通常有几种主流方案,下面我给你详细梳理一下常见的实现方式及其适用场景。

核心思路

数据权限的本质是:不同用户或角色查询同一张表时,返回不同的数据行

  • 销售经理能看到所有销售订单
  • 普通销售员只能看到自己的订单
  • 区域经理能看到本区域的订单

常见实现方案

方案1:基于MyBatis-Plus的拦截器(最常用)

这是目前最主流的方案,通过自定义拦截器在SQL执行前动态拼接权限条件。

// 1. 自定义拦截器实现 InnerInterceptor
@Component
public class DataPermissionInterceptor implements InnerInterceptor {
    @Override
    public void beforeQuery(Executor executor, MappedStatement ms, Object parameter, 
                           RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) {
        // 获取当前用户
        User currentUser = SecurityUtils.getCurrentUser();
        // 判断是否需要数据权限过滤
        if (currentUser == null || currentUser.isAdmin()) {
            return; // 管理员不过滤
        }
        // 获取原始SQL
        String originalSql = boundSql.getSql();
        // 根据用户角色拼接权限条件
        String newSql = buildPermissionSql(originalSql, currentUser);
        // 通过反射修改BoundSql中的sql
        ReflectUtil.setFieldValue(boundSql, "sql", newSql);
    }
    private String buildPermissionSql(String originalSql, User user) {
        // 根据用户角色添加不同的条件
        if (user.isDeptManager()) {
            return "SELECT * FROM (" + originalSql + ") tmp WHERE dept_id = " + user.getDeptId();
        } else {
            return "SELECT * FROM (" + originalSql + ") tmp WHERE create_by = " + user.getId();
        }
    }
}

配置到MyBatis-Plus:

@Configuration
public class MybatisPlusConfig {
    @Bean
    public MybatisPlusInterceptor mybatisPlusInterceptor() {
        MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
        interceptor.addInnerInterceptor(new DataPermissionInterceptor());
        return interceptor;
    }
}

方案2:基于AOP的自定义注解

通过注解标记需要权限控制的方法,在AOP中动态修改查询条件。

// 1. 定义注解
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface DataPermission {
    String tableAlias() default "";  // 表别名
    String field() default "org_id"; // 权限字段
}
// 2. AOP切面
@Aspect
@Component
public class DataPermissionAspect {
    @Around("@annotation(dataPermission)")
    public Object around(ProceedingJoinPoint joinPoint, DataPermission dataPermission) {
        // 获取当前用户的组织权限
        List<String> accessibleOrgs = getCurrentUserOrgIds();
        // 将组织ID列表存入ThreadLocal
        DataPermissionContext.setOrgIds(accessibleOrgs);
        DataPermissionContext.setTableAlias(dataPermission.tableAlias());
        DataPermissionContext.setField(dataPermission.field());
        try {
            return joinPoint.proceed();
        } finally {
            DataPermissionContext.clear();
        }
    }
}
// 3. 在Mapper中使用
@Mapper
public interface OrderMapper {
    @DataPermission(tableAlias = "o", field = "org_id")
    @Select("SELECT * FROM orders o WHERE o.status = 'active'")
    List<Order> getActiveOrders();
}

方案3:基于RBAC+组织机构树的通用方案

这是企业级应用中最完善的方案,支持灵活的组织架构权限。

-- 权限表设计
CREATE TABLE sys_org (
    id BIGINT PRIMARY KEY,
    parent_id BIGINT,
    org_name VARCHAR(100),
    org_level INT,  -- 组织层级
    path VARCHAR(500)  -- 路径,如:/1/2/3/
);
CREATE TABLE sys_role_org (
    id BIGINT PRIMARY KEY,
    role_id BIGINT,
    org_id BIGINT,
    data_scope INT  -- 1:仅本人 2:本部门 3:本部门及子部门 4:全部
);
-- 查询逻辑
SELECT * FROM orders o
WHERE EXISTS (
    SELECT 1 FROM sys_role_org ro
    JOIN sys_user_role ur ON ur.role_id = ro.role_id
    WHERE ur.user_id = #{currentUserId}
    AND (
        (ro.data_scope = 1 AND o.create_by = #{currentUserId})
        OR 
        (ro.data_scope = 2 AND o.org_id = ro.org_id)
        OR 
        (ro.data_scope = 3 AND o.org_id IN (
            SELECT id FROM sys_org WHERE path LIKE (
                SELECT CONCAT(path, '%') FROM sys_org WHERE id = ro.org_id
            )
        ))
        OR 
        (ro.data_scope = 4)  -- 全部数据
    )
)

方案4:基于Sa-Token框架(推荐)

Sa-Token是一个轻量级Java权限框架,提供了开箱即用的数据权限解决方案。

<dependency>
    <groupId>cn.dev33</groupId>
    <artifactId>sa-token-spring-boot-starter</artifactId>
    <version>1.37.0</version>
</dependency>
// 1. 自定义数据权限接口
@Component
public class CustomDataPermission implements SaDataPermissionHandler {
    @Override
    public SaDataPermissionResult getDataPermission(List<String> roles) {
        SaDataPermissionResult result = new SaDataPermissionResult();
        // 根据角色返回不同的权限条件
        if (roles.contains("admin")) {
            result.setCondition("1=1");  // 全部数据
        } else if (roles.contains("dept_manager")) {
            result.setCondition("dept_id = " + StpUtil.getLoginIdAsLong());
        } else {
            result.setCondition("create_by = " + StpUtil.getLoginIdAsLong());
        }
        return result;
    }
}
// 2. 在Service中使用
@Service
public class OrderService {
    @SaCheckDataPermission(table = "orders")  // 自动拼接权限条件
    public List<Order> getOrders() {
        return orderMapper.selectList(null);
    }
}

缓存优化建议

数据权限查询通常涉及连表查询,建议做缓存优化:

@Component
public class UserPermissionCache {
    @Cacheable(value = "user_permission", key = "#userId", unless = "#result.isEmpty()")
    public List<Long> getUserAccessibleOrgIds(Long userId) {
        // 查询用户可访问的所有组织ID(包含子组织)
        return userMapper.getUserAccessibleOrgIds(userId);
    }
    @CacheEvict(value = "user_permission", key = "#userId")
    public void clearUserPermissionCache(Long userId) {
        // 权限变更时清除缓存
    }
}

最佳实践建议

场景 推荐方案 原因
新项目、快速迭代 Sa-Token框架 开箱即用,集成简单
已有MyBatis-Plus项目 拦截器方案 无需改代码,全局生效
复杂组织机构权限 RBAC+组织树 灵活度高,支持层级
简单场景(仅本人数据) MyBatis-Plus条件构造器 最简单,最低开销

注意事项

  1. 性能问题:在SQL中拼接子查询会影响性能,建议对大数据量表建立合适的索引
  2. 测试覆盖:要覆盖测试各种角色组合的权限边界
  3. 审计日志:记录权限变更日志,便于排查问题
  4. 兜底策略:如果权限系统故障,应该有降级方案(比如只显示自己的数据)

选择方案时,建议从项目当前阶段和未来预期需求出发,不要过度设计过早优化,初期可以先从简单的注解方案开始,随着系统复杂度提升再逐步升级到更完善的架构。

抱歉,评论功能暂时关闭!