Spring Boot整合MyBatis Plus实现多维度数据权限控制
目录
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;@Resourceprivate 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模板*/@Beanpublic RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory factory) {RedisTemplate<String, Object> template = new RedisTemplate<>();template.setConnectionFactory(factory);// 使用StringRedisSerializer来序列化和反序列化redis的keytemplate.setKeySerializer(new StringRedisSerializer());template.setHashKeySerializer(new StringRedisSerializer());// 使用GenericJackson2JsonRedisSerializer来序列化和反序列化redis的valuetemplate.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拦截器*/@Beanpublic 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 {@Resourceprivate MybatisPlusInterceptor mybatisPlusInterceptor;@Resourceprivate PermissionHandling permissionHandling;/*** 应用启动时执行,添加数据权限拦截器* * @param args 应用启动参数*/@Overridepublic void run(ApplicationArguments args) {List<InnerInterceptor> innerInterceptors = new ArrayList<>(mybatisPlusInterceptor.getInterceptors());innerInterceptors.add(0, new DataPermissionInterceptor(new InnerDataPermissionHandler(permissionHandling)));mybatisPlusInterceptor.setInterceptors(innerInterceptors);}/*** 内部数据权限处理器*/@RequiredArgsConstructorpublic static class InnerDataPermissionHandler implements MultiDataPermissionHandler {private final PermissionHandling permissionHandling;/*** 获取SQL片段,用于数据权限过滤* * @param table 表信息* @param where 原始WHERE条件* @param mappedStatementId Mapper语句ID* @return 数据权限过滤条件*/@Overridepublic 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: localhostport: 6379database: 0password:timeout: 3000mslettuce:pool:max-active: 8max-wait: -1msmax-idle: 8min-idle: 0# MyBatis Plus配置
mybatis-plus:mapper-locations: classpath*:mapper/**/*.xmltype-aliases-package: com.fantaibao.entityconfiguration:map-underscore-to-camel-case: truelog-impl: org.apache.ibatis.logging.stdout.StdOutImpl
系统特点
-
高性能:使用Redis缓存权限数据,减少数据库查询压力
-
灵活性:支持多种过滤类型(用户、门店、部门等),可根据业务需求灵活配置
-
安全性:使用MyBatis Plus的数据权限拦截器和JSqlParser,避免SQL注入风险
-
易用性:通过注解方式配置,代码清晰易懂,便于维护
-
可扩展性:系统设计支持多种权限过滤类型,可根据业务需求轻松扩展
使用说明
-
添加注解:在Mapper接口的方法上添加
@DataScope
注解,指定表别名、字段名和过滤类型 -
权限初始化:系统启动时通过ApplicationRunner配置MyBatis Plus的数据权限拦截器
-
权限缓存:权限数据会自动缓存到Redis,减少数据库查询压力
-
SQL修改:MyBatis Plus拦截器会自动修改SQL,添加数据权限过滤条件
扩展建议
-
权限变更通知:实现权限变更时的缓存清除机制,确保数据一致性
-
多级缓存:可以添加本地缓存作为Redis缓存的前置缓存,进一步提高性能
-
权限管理界面:开发权限管理界面,动态配置用户数据权限
-
性能监控:添加权限过滤的性能监控日志,优化权限查询逻辑
这个实现基于MyBatis Plus的数据权限拦截器和Redis缓存,提供了高效、安全的数据权限过滤解决方案,适用于企业级应用的多租户、数据隔离等场景。