使用MyBatis-Plus实现数据权限功能
什么是数据权限
数据权限是指系统根据用户的角色、职位或其他属性,控制用户能够访问的数据范围。与传统的功能权限(菜单、按钮权限)不同,数据权限关注的是数据行级别的访问控制。
常见的数据权限控制方式包括:
-
部门数据权限:只能访问本部门数据
-
个人数据权限:只能访问自己的数据
-
自定义数据范围:通过特定规则限制数据访问
MyBatis-Plus实现数据权限的方案
实现完整例子
下面是一个完整的基于MyBatis-Plus和Spring Security的数据权限实现示例:
1. 数据权限配置类
@Configuration
public class MybatisConfig {@Beanpublic ConfigurationCustomizer mybatisConfigurationCustomizer(){return configuration -> configuration.setObjectWrapperFactory(new MapWrapperFactory());}@Beanpublic MybatisPlusInterceptor mybatisPlusInterceptor() {MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();// 1.添加数据权限插件interceptor.addInnerInterceptor(new DataPermissionInterceptor(new DataPressionConfig()));PaginationInnerInterceptor paginationInnerInterceptor = new PaginationInnerInterceptor();// 分页插件paginationInnerInterceptor.setOptimizeJoin(false);paginationInnerInterceptor.setOverflow(true);paginationInnerInterceptor.setDbType(DbType.POSTGRE_SQL);interceptor.addInnerInterceptor(paginationInnerInterceptor);return interceptor;}
}
2. 数据权限拦截器
@Slf4j
@Component
public class DataPressionConfig implements DataPermissionHandler {@Overridepublic Expression getSqlSegment(Expression where, String mappedStatementId) {try {if(null==mappedStatementId){return null;}Class<?> mapperClazz = Class.forName(mappedStatementId.substring(0, mappedStatementId.lastIndexOf(".")));String methodName = mappedStatementId.substring(mappedStatementId.lastIndexOf(".") + 1);// 获取自身类中的所有方法,不包括继承。与访问权限无关Method[] methods = mapperClazz.getDeclaredMethods();for (Method method : methods) {DataScope dataScopeAnnotationMethod = method.getAnnotation(DataScope.class);if(null==dataScopeAnnotationMethod){continue;}//spring aoc里拿方法参数Parameter[] parameters= method.getParameters();if (parameters.length > 0) {log.info("方法参数:" + dataScopeAnnotationMethod.oneselfScopeName() );}if (ObjectUtils.isEmpty(dataScopeAnnotationMethod) || !dataScopeAnnotationMethod.enabled()) {continue;}if (method.getName().equals(methodName) || (method.getName() + "_COUNT").equals(methodName) || (method.getName() + "_count").equals(methodName)) {return buildDataScopeByAnnotation(dataScopeAnnotationMethod,mappedStatementId);}}} catch (ClassNotFoundException e) {e.printStackTrace();}return null;}/*** DataScope注解方式,拼装数据权限** @param dataScope* @return*/private Expression buildDataScopeByAnnotation(DataScope dataScope,String mapperId) {Map<String, Object> params = DataPermissionContext.getParams();if (params == null || params.isEmpty()) {return null;}Object areaCodes = params.get(mapperId);List<String> dataScopeDeptIds= (List<String>) areaCodes;// 获取注解信息String tableAlias = dataScope.tableAlias();String areaCodes= dataScope.areaCodes();Expression expression = buildDataScopeExpression(tableAlias, areaCodes, dataScopeDeptIds);return expression == null ? null : new Parenthesis(expression);}/*** 拼装数据权限** @param tableAlias 表别名* @param oneselfScopeName 本人限制范围的字段名称* @param dataScopeDeptIds 数据权限部门ID集合,去重* @return*/private Expression buildDataScopeExpression(String tableAlias, String areaCodes, List<String> dataScopeDeptIds) {/*** 构造部门里行政区划 area_code 的in表达式。*/try {String sql=tableAlias + "." + areaCodes+" in (";for(String areaCode:dataScopeDeptIds){sql+="'"+areaCode+"',";}sql=sql.substring(0,sql.length()-1)+")";Expression selectExpression = CCJSqlParserUtil.parseCondExpression(sql, true);return selectExpression;} catch (JSQLParserException e) {throw new RuntimeException(e);}}}
3. 存储spring aop切面拿到的数据
public class DataPermissionContext {private static final ThreadLocal<Map<String, Object>> CONTEXT = new ThreadLocal<>();public static void setParams(Map<String, Object> params) {CONTEXT.set(params);}public static Map<String, Object> getParams() {return CONTEXT.get();}public static void clear() {CONTEXT.remove();}
}
4. 定义数据权限注解
@Inherited
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface DataScope {/*** 是否生效,默认true-生效*/boolean enabled() default true;/*** 表别名*/String tableAlias() default "";/*** 本人限制范围的字段名称*/String areaCodes() default "area_code";}
5. 实现AOP切面
@Slf4j
@Aspect
@Component
public class DataAspet {@Pointcut("@annotation(DataScope)")public void logPoinCut() {}@Before("logPoinCut()")public void saveSysLog(JoinPoint joinPoint) {//从切面织入点处通过反射机制获取织入点处的方法MethodSignature signature = (MethodSignature) joinPoint.getSignature();//获取切入点所在的方法Method method = signature.getMethod();DataScope scope = method.getAnnotation(DataScope.class);Map<String, Object> paramValues = new HashMap<>();Object[] args = joinPoint.getArgs();Parameter[] parameters = method.getParameters();if(null!=parameters&¶meters.length>0) {//添加到最后一个参数log.info("参数名称:{}",signature.getName()+":"+signature.getDeclaringTypeName());paramValues.put(signature.getDeclaringTypeName()+"."+signature.getName(), args[parameters.length-1]);DataPermissionContext.setParams(paramValues);}else{DataPermissionContext.setParams(null);}}}
6. mapper里注解使用数据权限
tableAlias里你的sql里面要插入数据权限字段的表别名。
比如:
select count(*) as total,d.type as type from bus_device d group by d.type
spring aop 会读取mapper方法最后一个参数,然后切入Sql变成
select count(*) as total,d.type as type from bus_device d where
d.area_code in(#{areaCodes} ) group by d.type
@DataScope(tableAlias = "d")List<DeviceTypeCountDto> listByApplicationCategory(@Param( @Param("type") String type,List<String> areaCodes);
注意事项
-
性能考虑:数据权限过滤会增加SQL复杂度,可能影响查询性能,特别是对于大数据量表。可以考虑添加适当的索引优化。
-
SQL注入风险:在拼接SQL时要特别注意防止SQL注入,建议使用预编译参数。
-
缓存问题:如果使用了缓存,需要注意数据权限可能导致缓存命中率下降或数据泄露问题。
-
多租户场景:在多租户系统中,数据权限通常需要与租户隔离一起考虑。
-
复杂查询:对于复杂的多表关联查询,数据权限条件可能需要更精细的控制。
通过以上方案,我们可以灵活地在MyBatis-Plus中实现各种数据权限控制需求,根据项目实际情况选择最适合的实现方式。