Spring Boot整合MyBatis Plus实现多维度数据权限控制

发布于:2025-09-02 ⋅ 阅读:(16) ⋅ 点赞:(0)

目录

1. 数据权限注解 (DataScope.java)

2. 过滤类型枚举 (FilterWhereTypeEnum.java)

3. 权限处理服务 (PermissionHandling.java)

4. Redis配置类 (RedisConfig.java)

5. MyBatis Plus配置 (MybatisPlusConfig.java)

6. 权限拦截器配置 (PermissionRunner.java)

7. 使用示例 (TeachingMapper.java)

8. 配置文件 (application.yml)


1. 数据权限注解 (DataScope.java)

package com.fantaibao.permission.annotation;

import com.fantaibao.permission.enums.FilterWhereTypeEnum;

import java.lang.annotation.*;

/**
 * 数据权限过滤注解
 * 
 * @author fantai
 * @date 2023/07/01
 */
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface DataScope {
    
    /**
     * 表别名
     */
    String tableAlias() default "";
    
    /**
     * 表字段名
     */
    String tableField() default "user_id";
    
    /**
     * 过滤类型
     */
    FilterWhereTypeEnum type() default FilterWhereTypeEnum.USER_FILTER;
    
    /**
     * 是否启用数据权限过滤
     */
    boolean enabled() default true;
}

2. 过滤类型枚举 (FilterWhereTypeEnum.java)

package com.fantaibao.permission.enums;

/**
 * 数据权限过滤类型枚举
 * 
 * @author fantai
 * @date 2023/07/01
 */
public enum FilterWhereTypeEnum {
    
    /**
     * 用户过滤 - 基于用户ID
     */
    USER_FILTER,
    
    /**
     * 门店过滤 - 基于门店ID
     */
    STORE_FILTER,
    
    /**
     * 部门过滤 - 基于部门ID
     */
    DEPT_FILTER,
    
    /**
     * 角色过滤 - 基于角色ID
     */
    ROLE_FILTER
}

3. 权限处理服务 (PermissionHandling.java)

package com.fantaibao.permission.handling;

import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.ValueOperations;
import org.springframework.stereotype.Service;
import org.springframework.util.CollectionUtils;

import javax.annotation.Resource;
import java.util.Collections;
import java.util.List;
import java.util.concurrent.TimeUnit;

/**
 * 权限处理服务 - 使用Redis缓存权限数据
 * 
 * @author fantai
 * @date 2023/07/01
 */
@Service
public class PermissionHandling {
    
    private static final String USER_PERMISSION_KEY_PREFIX = "user:permissions:";
    private static final long CACHE_EXPIRE_HOURS = 1;
    
    @Resource
    private RedisTemplate<String, Object> redisTemplate;
    
    // 假设有用户服务或门店服务来获取实际数据
    // @Resource
    // private UserService userService;
    // 
    // @Resource
    // private StoreService storeService;
    
    /**
     * 根据用户ID获取有权限的用户ID列表
     * 
     * @param userId 用户ID
     * @return 有权限的用户ID列表
     */
    public List<String> getUserIdsByUserId(String userId) {
        String cacheKey = USER_PERMISSION_KEY_PREFIX + "userIds:" + userId;
        
        // 先从Redis获取
        ValueOperations<String, Object> ops = redisTemplate.opsForValue();
        List<String> userIds = (List<String>) ops.get(cacheKey);
        
        if (!CollectionUtils.isEmpty(userIds)) {
            return userIds;
        }
        
        // Redis中没有,从数据库获取
        // userIds = userService.getPermissionUserIds(userId);
        // 这里使用模拟数据
        userIds = List.of("1001", "1002", "1003");
        
        // 存入Redis,设置过期时间
        if (!CollectionUtils.isEmpty(userIds)) {
            ops.set(cacheKey, userIds, CACHE_EXPIRE_HOURS, TimeUnit.HOURS);
        }
        
        return userIds != null ? userIds : Collections.emptyList();
    }
    
    /**
     * 根据用户ID获取有权限的门店ID列表
     * 
     * @param userId 用户ID
     * @return 有权限的门店ID列表
     */
    public List<String> getStoreIdsByUserId(String userId) {
        String cacheKey = USER_PERMISSION_KEY_PREFIX + "storeIds:" + userId;
        
        // 先从Redis获取
        ValueOperations<String, Object> ops = redisTemplate.opsForValue();
        List<String> storeIds = (List<String>) ops.get(cacheKey);
        
        if (!CollectionUtils.isEmpty(storeIds)) {
            return storeIds;
        }
        
        // Redis中没有,从数据库获取
        // storeIds = storeService.getPermissionStoreIds(userId);
        // 这里使用模拟数据
        storeIds = List.of("2001", "2002", "2003");
        
        // 存入Redis,设置过期时间
        if (!CollectionUtils.isEmpty(storeIds)) {
            ops.set(cacheKey, storeIds, CACHE_EXPIRE_HOURS, TimeUnit.HOURS);
        }
        
        return storeIds != null ? storeIds : Collections.emptyList();
    }
    
    /**
     * 根据用户ID获取有权限的部门ID列表
     * 
     * @param userId 用户ID
     * @return 有权限的部门ID列表
     */
    public List<String> getDeptIdsByUserId(String userId) {
        String cacheKey = USER_PERMISSION_KEY_PREFIX + "deptIds:" + userId;
        
        // 先从Redis获取
        ValueOperations<String, Object> ops = redisTemplate.opsForValue();
        List<String> deptIds = (List<String>) ops.get(cacheKey);
        
        if (!CollectionUtils.isEmpty(deptIds)) {
            return deptIds;
        }
        
        // Redis中没有,从数据库获取
        // deptIds = deptService.getPermissionDeptIds(userId);
        // 这里使用模拟数据
        deptIds = List.of("3001", "3002", "3003");
        
        // 存入Redis,设置过期时间
        if (!CollectionUtils.isEmpty(deptIds)) {
            ops.set(cacheKey, deptIds, CACHE_EXPIRE_HOURS, TimeUnit.HOURS);
        }
        
        return deptIds != null ? deptIds : Collections.emptyList();
    }
    
    /**
     * 清除用户权限缓存
     * 
     * @param userId 用户ID
     */
    public void clearUserPermissionCache(String userId) {
        String userKey = USER_PERMISSION_KEY_PREFIX + "userIds:" + userId;
        String storeKey = USER_PERMISSION_KEY_PREFIX + "storeIds:" + userId;
        String deptKey = USER_PERMISSION_KEY_PREFIX + "deptIds:" + userId;
        
        redisTemplate.delete(userKey);
        redisTemplate.delete(storeKey);
        redisTemplate.delete(deptKey);
    }
}

4. Redis配置类 (RedisConfig.java)

package com.fantaibao.permission.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer;

/**
 * Redis配置类
 * 
 * @author fantai
 * @date 2023/07/01
 */
@Configuration
public class RedisConfig {
    
    /**
     * 配置Redis模板
     * 
     * @param factory Redis连接工厂
     * @return Redis模板
     */
    @Bean
    public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory factory) {
        RedisTemplate<String, Object> template = new RedisTemplate<>();
        template.setConnectionFactory(factory);
        
        // 使用StringRedisSerializer来序列化和反序列化redis的key
        template.setKeySerializer(new StringRedisSerializer());
        template.setHashKeySerializer(new StringRedisSerializer());
        
        // 使用GenericJackson2JsonRedisSerializer来序列化和反序列化redis的value
        template.setValueSerializer(new GenericJackson2JsonRedisSerializer());
        template.setHashValueSerializer(new GenericJackson2JsonRedisSerializer());
        
        template.afterPropertiesSet();
        return template;
    }
}

5. MyBatis Plus配置 (MybatisPlusConfig.java)

package com.fantaibao.permission.config;

import com.baomidou.mybatisplus.annotation.DbType;
import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor;
import com.baomidou.mybatisplus.extension.plugins.inner.PaginationInnerInterceptor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

/**
 * MyBatis Plus配置类
 * 
 * @author fantai
 * @date 2023/07/01
 */
@Configuration
public class MybatisPlusConfig {

    /**
     * 配置MyBatis Plus拦截器
     * 
     * @return MyBatis Plus拦截器
     */
    @Bean
    public MybatisPlusInterceptor mybatisPlusInterceptor() {
        MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
        // 添加分页插件
        interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL));
        return interceptor;
    }
}

6. 权限拦截器配置 (PermissionRunner.java)

package com.fantaibao.permission.config;

import com.baomidou.mybatisplus.core.toolkit.ObjectUtils;
import com.baomidou.mybatisplus.core.toolkit.StringUtils;
import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor;
import com.baomidou.mybatisplus.extension.plugins.handler.MultiDataPermissionHandler;
import com.baomidou.mybatisplus.extension.plugins.inner.DataPermissionInterceptor;
import com.baomidou.mybatisplus.extension.plugins.inner.InnerInterceptor;
import com.fantaibao.permission.annotation.DataScope;
import com.fantaibao.permission.enums.FilterWhereTypeEnum;
import com.fantaibao.permission.handling.PermissionHandling;
import jnpf.base.UserInfo;
import jnpf.util.UserProvider;
import lombok.RequiredArgsConstructor;
import net.sf.jsqlparser.expression.Expression;
import net.sf.jsqlparser.expression.StringValue;
import net.sf.jsqlparser.expression.operators.conditional.AndExpression;
import net.sf.jsqlparser.expression.operators.relational.ExpressionList;
import net.sf.jsqlparser.expression.operators.relational.InExpression;
import net.sf.jsqlparser.schema.Column;
import net.sf.jsqlparser.schema.Table;
import org.springframework.boot.ApplicationArguments;
import org.springframework.boot.ApplicationRunner;
import org.springframework.stereotype.Component;

import javax.annotation.Resource;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;

/**
 * 权限拦截器配置 - 应用启动时初始化数据权限拦截器
 * 
 * @author fantai
 * @date 2023/07/01
 */
@Component
public class PermissionRunner implements ApplicationRunner {

    @Resource
    private MybatisPlusInterceptor mybatisPlusInterceptor;

    @Resource
    private PermissionHandling permissionHandling;

    /**
     * 应用启动时执行,添加数据权限拦截器
     * 
     * @param args 应用启动参数
     */
    @Override
    public void run(ApplicationArguments args) {
        List<InnerInterceptor> innerInterceptors = new ArrayList<>(mybatisPlusInterceptor.getInterceptors());
        innerInterceptors.add(0, new DataPermissionInterceptor(new InnerDataPermissionHandler(permissionHandling)));
        mybatisPlusInterceptor.setInterceptors(innerInterceptors);
    }

    /**
     * 内部数据权限处理器
     */
    @RequiredArgsConstructor
    public static class InnerDataPermissionHandler implements MultiDataPermissionHandler {

        private final PermissionHandling permissionHandling;

        /**
         * 获取SQL片段,用于数据权限过滤
         * 
         * @param table 表信息
         * @param where 原始WHERE条件
         * @param mappedStatementId Mapper语句ID
         * @return 数据权限过滤条件
         */
        @Override
        public Expression getSqlSegment(Table table, Expression where, String mappedStatementId) {
            try {
                Class<?> mapperClazz = Class.forName(mappedStatementId.substring(0, mappedStatementId.lastIndexOf(".")));
                String methodName = mappedStatementId.substring(mappedStatementId.lastIndexOf(".") + 1);
                
                // 获取Mapper类中声明的方法
                Method[] methods = mapperClazz.getDeclaredMethods();
                if (methods.length == 0) {
                    return null;
                }
                
                // 查找目标方法
                Method targetMethod = Arrays.stream(methods)
                        .filter(method -> method.getName().equals(methodName))
                        .findFirst()
                        .orElse(null);
                
                if (targetMethod == null) {
                    return null;
                }
                
                // 检查方法是否包含DataScope注解
                DataScope dataScopeAnnotation = targetMethod.getAnnotation(DataScope.class);
                if (ObjectUtils.isEmpty(dataScopeAnnotation) || !dataScopeAnnotation.enabled()) {
                    return null;
                }
                
                // 跳过JOIN中的ON条件表达式拼装
                if (isJoinOnCondition(where)) {
                    return null;
                }
                
                // 构建数据权限过滤条件
                return buildDataScopeByAnnotation(dataScopeAnnotation);
            } catch (Exception e) {
                throw new RuntimeException("数据权限处理失败: " + e.getMessage(), e);
            }
        }

        /**
         * 判断是否为JOIN ON条件
         * 
         * @param where WHERE条件表达式
         * @return 是否为JOIN ON条件
         */
        private boolean isJoinOnCondition(Expression where) {
            if (where == null) {
                return false;
            }
            
            // 处理AND表达式的情况
            if (where.getASTNode() == null && where instanceof AndExpression) {
                Expression leftExpression = ((AndExpression) where).getLeftExpression();
                if (leftExpression.getASTNode() != null && 
                    "RegularCondition".equals(leftExpression.getASTNode().toString()) &&
                    "JoinerExpression".equals(leftExpression.getASTNode().jjtGetParent().jjtGetParent().toString())) {
                    return true;
                }
            }
            
            // 处理普通表达式的情况
            if (where.getASTNode() != null && 
                "JoinerExpression".equals(where.getASTNode().jjtGetParent().jjtGetParent().toString())) {
                return true;
            }
            
            return false;
        }

        /**
         * 根据注解构建数据权限过滤表达式
         * 
         * @param dataScopeAnnotation DataScope注解
         * @return 数据权限过滤表达式
         */
        private Expression buildDataScopeByAnnotation(DataScope dataScopeAnnotation) {
            UserInfo userInfo = UserProvider.getUser();
            
            // 管理员拥有所有权限,不需要过滤
            if (userInfo.getIsAdministrator()) {
                return null;
            }
            
            // 获取权限ID列表
            List<String> ids = getPermissionIds(dataScopeAnnotation.type(), userInfo.getUserId());
            
            // 权限适用范围为全部或为空时,不需要过滤
            if (ids == null || ids.isEmpty()) {
                return null;
            }
            
            // 构建IN表达式
            InExpression inExpression = new InExpression();
            ExpressionList expressionList = new ExpressionList();
            
            // 添加权限值到表达式列表
            ids.forEach(id -> expressionList.addExpressions(new StringValue(id)));
            
            // 设置字段表达式和值列表
            inExpression.setLeftExpression(buildColumn(dataScopeAnnotation.tableAlias(), dataScopeAnnotation.tableField()));
            inExpression.setRightItemsList(expressionList);
            
            return inExpression;
        }

        /**
         * 根据过滤类型获取权限ID列表
         * 
         * @param type 过滤类型
         * @param userId 用户ID
         * @return 权限ID列表
         */
        private List<String> getPermissionIds(FilterWhereTypeEnum type, String userId) {
            switch (type) {
                case USER_FILTER:
                    return permissionHandling.getUserIdsByUserId(userId);
                case STORE_FILTER:
                    return permissionHandling.getStoreIdsByUserId(userId);
                case DEPT_FILTER:
                    return permissionHandling.getDeptIdsByUserId(userId);
                default:
                    return Collections.emptyList();
            }
        }

        /**
         * 构建字段表达式
         * 
         * @param tableAlias 表别名
         * @param columnName 字段名称
         * @return 字段表达式
         */
        private Column buildColumn(String tableAlias, String columnName) {
            if (StringUtils.isNotEmpty(tableAlias)) {
                columnName = tableAlias + "." + columnName;
            }
            return new Column(columnName);
        }
    }
}

7. 使用示例 (TeachingMapper.java)

package com.fantaibao.mapper;

import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.fantaibao.permission.annotation.DataScope;
import com.fantaibao.permission.enums.FilterWhereTypeEnum;
import com.fantaibao.entity.TeachingRecord;
import com.fantaibao.vo.RecordPageListVo;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;

import java.util.List;

/**
 * 教学记录Mapper
 * 
 * @author fantai
 * @date 2023/07/01
 */
@Mapper
public interface TeachingMapper extends BaseMapper<TeachingRecord> {
    
    /**
     * 分页查询教学记录列表 - 基于用户权限过滤
     * 
     * @param pageDto 查询条件
     * @return 教学记录列表
     */
    @DataScope(tableAlias = "fctr", tableField = "F_UserId", type = FilterWhereTypeEnum.USER_FILTER)
    List<RecordPageListVo> recordPageList(@Param("pageDto") TeachingBaseFilter pageDto);
    
    /**
     * 分页查询教学记录列表 - 基于门店权限过滤
     * 
     * @param pageDto 查询条件
     * @return 教学记录列表
     */
    @DataScope(tableAlias = "fctr", tableField = "F_StoreId", type = FilterWhereTypeEnum.STORE_FILTER)
    List<RecordPageListVo> recordPageListByStore(@Param("pageDto") TeachingBaseFilter pageDto);
    
    /**
     * 分页查询教学记录列表 - 基于部门权限过滤
     * 
     * @param pageDto 查询条件
     * @return 教学记录列表
     */
    @DataScope(tableAlias = "fctr", tableField = "F_DeptId", type = FilterWhereTypeEnum.DEPT_FILTER)
    List<RecordPageListVo> recordPageListByDept(@Param("pageDto") TeachingBaseFilter pageDto);
}

8. 配置文件 (application.yml)

spring:
  redis:
    host: localhost
    port: 6379
    database: 0
    password:
    timeout: 3000ms
    lettuce:
      pool:
        max-active: 8
        max-wait: -1ms
        max-idle: 8
        min-idle: 0

# MyBatis Plus配置
mybatis-plus:
  mapper-locations: classpath*:mapper/**/*.xml
  type-aliases-package: com.fantaibao.entity
  configuration:
    map-underscore-to-camel-case: true
    log-impl: org.apache.ibatis.logging.stdout.StdOutImpl

系统特点

  1. 高性能:使用Redis缓存权限数据,减少数据库查询压力

  2. 灵活性:支持多种过滤类型(用户、门店、部门等),可根据业务需求灵活配置

  3. 安全性:使用MyBatis Plus的数据权限拦截器和JSqlParser,避免SQL注入风险

  4. 易用性:通过注解方式配置,代码清晰易懂,便于维护

  5. 可扩展性:系统设计支持多种权限过滤类型,可根据业务需求轻松扩展

使用说明

  1. 添加注解:在Mapper接口的方法上添加@DataScope注解,指定表别名、字段名和过滤类型

  2. 权限初始化:系统启动时通过ApplicationRunner配置MyBatis Plus的数据权限拦截器

  3. 权限缓存:权限数据会自动缓存到Redis,减少数据库查询压力

  4. SQL修改:MyBatis Plus拦截器会自动修改SQL,添加数据权限过滤条件

扩展建议

  1. 权限变更通知:实现权限变更时的缓存清除机制,确保数据一致性

  2. 多级缓存:可以添加本地缓存作为Redis缓存的前置缓存,进一步提高性能

  3. 权限管理界面:开发权限管理界面,动态配置用户数据权限

  4. 性能监控:添加权限过滤的性能监控日志,优化权限查询逻辑

这个实现基于MyBatis Plus的数据权限拦截器和Redis缓存,提供了高效、安全的数据权限过滤解决方案,适用于企业级应用的多租户、数据隔离等场景。