数据权限的设计与实现系列3——MybatisPlus数据权限插件实现机制及使用示例

背景

上篇我们深度剖析了若依开发平台关于数据权限的设计与实现,并指出了其模式的局限性。
今天我们来看一下MyBatisPlus提供的数据权限插件的实现机制与使用示例。
官方说明:https://baomidou.com/plugins/data-permission/

实现机制

在 SQL 执行前拦截 SQL 语句,并根据用户权限动态添加权限相关的 SQL 片段。这样,只有用户有权限访问的数据才会被查询出来。
拦截SQL语句是基于拦截器技术实现的,修改SQL语句是基于开源的 SQL 解析库JSQLParser完成的

JSQLParser的解析功能示例如下:

// 示例 SQL
String sql = "SELECT * FROM user WHERE status = 'active'";
Expression expression;

try {
    expression = CCJSqlParserUtil.parseCondExpression("status = 'inactive'");
    PlainSelect select = (PlainSelect) ((Select) CCJSqlParserUtil.parse(sql)).getSelectBody();
    select.setWhere(expression);

    System.out.println(select); // 输出:SELECT * FROM user WHERE status = 'inactive'
} catch (JSQLParserException e) {
    e.printStackTrace();
}

使用方式

1.实现数据权限逻辑

官方提供了一个MultiDataPermissionHandler类,需要继承该类,覆写getSqlSegment方法,自行处理数据权限逻辑,也就是需要追加的数据权限过滤的SQL片段。

public class CustomDataPermissionHandler extends MultiDataPermissionHandler {
    @Override
    public Expression getSqlSegment(Table table, Expression where, String mappedStatementId) {
        // 在此处编写自定义数据权限逻辑
        try {
            String sqlSegment = "..."; // 数据权限相关的 SQL 片段
            return CCJSqlParserUtil.parseCondExpression(sqlSegment);
        } catch (JSQLParserException e) {
            e.printStackTrace();
            return null;
        }
    }
}

2.注册数据权限拦截器

修改MybatisPlus拦截器的配置类,将数据权限插件加入进去,并且注意需要放在分页插件之前。

    @Bean
    public MybatisPlusInterceptor mybatisPlusInterceptor() {
        MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
        // 1.添加数据权限插件
        interceptor.addInnerInterceptor(new DataPermissionInterceptor(new CustomDataPermissionHandler ()));
        // 2.添加分页插件
        PaginationInnerInterceptor pageInterceptor = new PaginationInnerInterceptor();   
        interceptor.addInnerInterceptor(pageInterceptor);
        return interceptor;
    }

实际上,MybatisPlus的数据权限插件,只提供了一个框架,并不是完整实现,核心的数据权限逻辑部分,需要自行实现。

数据权限逻辑的实现

打算找个具体的例子来补全拼图,去网上搜了大量资料,最终找到了这么一篇相对完整的示例。
https://blog.csdn.net/qq_42402854/article/details/139099661
整体实现思路与上篇提到的若依开发平台类似,但也存在一些差异,做了一些改进,下面具体来说说。

定义数据权限范围

同样设定了5种数据范围,使用枚举类型定义,相比若依平台的5个零散的字符串常量,更优雅一些。
数据权限的设计与实现系列3——MybatisPlus数据权限插件实现机制及使用示例_第1张图片

缓存数据权限

若依开发平台是在业务实体的基类的params属性中,通过一个约定键名为dataScope来存放需要追加的SQL片段。
本示例中则是扩展了权限控制组件SpringSecurity的用户对象UserDetails,增加了数据权限的属性,即在用户登录,系统认证完成后,查询当前用户所拥有的角色,进而查询角色对应的数据权限范围,然后存到用户对象中备用。
数据权限的设计与实现系列3——MybatisPlus数据权限插件实现机制及使用示例_第2张图片

定义数据权限注解

与若依平台实现,有所差异和改进。
一方面,使用一个公用属性tableAlias来处理部门表和用户表别名问题,而不是若依开发平台中的两个,意味着某些场景下复杂SQL语句如果同时涉及到部门表和用户表,则无法控制。不过是否真的存在这种场景存疑,主要是逻辑处理中是或关系,同时设置了部门维度和用户维度,实质上也会造成数据范围的扩大吧。
另一方面,若依平台只考虑了表别名问题,字段名则是硬编码到处理逻辑中去了,隐性要求业务表中部门id必须是dept_id,用户id必须是user_id。本示例中则考虑了该问题,进行了改进,使用deptScopeName来定义部门字段名,oneselfScopeName来定义用户字段名。
数据权限的设计与实现系列3——MybatisPlus数据权限插件实现机制及使用示例_第3张图片

实现数据权限逻辑

接下来就是最关键的部分,也就是MybatisPlus数据权限插件中要求的自定义权限逻辑处理器的实现,源码如下:

/**
 * 数据权限拼装逻辑处理
 *
 */
public class DataScopeHandler implements MultiDataPermissionHandler {

    /**
     * 获取数据权限 SQL 片段。
     * 

旧的 {@link MultiDataPermissionHandler#getSqlSegment(Expression, String)} 方法第一个参数包含所有的 where 条件信息,如果 return 了 null 会覆盖原有的 where 数据,

*

新版的 {@link MultiDataPermissionHandler#getSqlSegment(Table, Expression, String)} 方法不能覆盖原有的 where 数据,如果 return 了 null 则表示不追加任何 where 条件

* * @param table 所执行的数据库表信息,可以通过此参数获取表名和表别名 * @param where 原有的 where 条件信息 * @param mappedStatementId Mybatis MappedStatement Id 根据该参数可以判断具体执行方法 * @return JSqlParser 条件表达式,返回的条件表达式会拼接在原有的表达式后面(不会覆盖原有的表达式) */
@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); /** * DataScope注解优先级:【类上 > 方法上】 */ // 获取 DataScope注解 DataScope dataScopeAnnotationClazz = mapperClazz.getAnnotation(DataScope.class); if (ObjectUtils.isNotEmpty(dataScopeAnnotationClazz) && dataScopeAnnotationClazz.enabled()) { return buildDataScopeByAnnotation(dataScopeAnnotationClazz); } // 获取自身类中的所有方法,不包括继承。与访问权限无关 Method[] methods = mapperClazz.getDeclaredMethods(); for (Method method : methods) { DataScope dataScopeAnnotationMethod = method.getAnnotation(DataScope.class); 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); } } } catch (ClassNotFoundException e) { e.printStackTrace(); } return null; } /** * DataScope注解方式,拼装数据权限 * * @param dataScope * @return */ private Expression buildDataScopeByAnnotation(DataScope dataScope) { // 获取 UserDetails用户信息 Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); if (authentication == null) { return null; } Map<String, Object> userDetailsMap = BeanUtil.beanToMap(authentication.getPrincipal()); Set<String> dataScopeTypes = (Set<String>) userDetailsMap.get("dataScopeTypes"); Set<Long> dataScopeDeptIds = (Set<Long>) userDetailsMap.get("dataScopeDeptIds"); Long dataScopeCreateId = (Long) userDetailsMap.get("dataScopeCreateId"); // 获取注解信息 String tableAlias = dataScope.tableAlias(); String deptScopeName = dataScope.deptScopeName(); String oneselfScopeName = dataScope.oneselfScopeName(); Expression expression = buildDataScopeExpression(tableAlias, deptScopeName, oneselfScopeName, dataScopeDeptIds, dataScopeCreateId); return expression == null ? null : new Parenthesis(expression); } /** * 拼装数据权限 * * @param tableAlias 表别名 * @param deptScopeName 部门限制范围的字段名称 * @param oneselfScopeName 本人限制范围的字段名称 * @param dataScopeDeptIds 数据权限部门ID集合,去重 * @param dataScopeCreateId 数据权限本人ID * @return */ private Expression buildDataScopeExpression(String tableAlias, String deptScopeName, String oneselfScopeName, Set<Long> dataScopeDeptIds, Long dataScopeCreateId) { /** * 构造部门in表达式。 */ InExpression deptIdInExpression = null; if (CollectionUtils.isNotEmpty(dataScopeDeptIds)) { deptIdInExpression = new InExpression(); ExpressionList deptIds = new ExpressionList(dataScopeDeptIds.stream().map(LongValue::new).collect(Collectors.toList())); // 设置左边的字段表达式,右边设置值。 deptIdInExpression.setLeftExpression(buildColumn(tableAlias, deptScopeName)); deptIdInExpression.setRightExpression(new Parenthesis(deptIds)); } /** * 构造本人eq表达式 */ EqualsTo oneselfEqualsTo = null; if (dataScopeCreateId != null) { oneselfEqualsTo = new EqualsTo(); oneselfEqualsTo.withLeftExpression(buildColumn(tableAlias, oneselfScopeName)); oneselfEqualsTo.setRightExpression(new LongValue(dataScopeCreateId)); } if (deptIdInExpression != null && oneselfEqualsTo != null) { return new OrExpression(deptIdInExpression, oneselfEqualsTo); } else if (deptIdInExpression != null && oneselfEqualsTo == null) { return deptIdInExpression; } else if (deptIdInExpression == null && oneselfEqualsTo != null) { return oneselfEqualsTo; } return null; } /** * 构建Column * * @param tableAlias 表别名 * @param columnName 字段名称 * @return 带表别名字段 */ public static Column buildColumn(String tableAlias, String columnName) { if (StringUtils.isNotEmpty(tableAlias)) { columnName = tableAlias + "." + columnName; } return new Column(columnName); } }

代码中的注释写的比较详细,这里不再赘述,有几个关键点提一下:
1.对于getSqlSegment方法的入参,具体什么含义,怎么用,MybatisPlus官网其实没提,而示例给了详细的注释和说明,这一点挺好。特别是第三个参数mappedStatementId,可以根据该参数使用反射,拿到执行的类和方法,以及其上的注解。
2.因为提前做了预处理(在登录环节将当前用户的数据权限查询出来放到用户对象中),使用的时候大大简化了处理逻辑。
3.该逻辑实际存在瑕疵,问题出在部门表和用户表别名公用一个属性,实际存在冲突,要完善其实也简单,拆开就好了,不麻烦。

总结

本文介绍了MybatisPlus的数据权限插件的机制和使用,并通过一个具体示例补全了关键环节数据权限控制逻辑的实现。
与上篇若依开发平台对比,主要区别在于技术实现方式不同。
若依开发平台是自行拼接SQL片段,MybatisPlus的数据权限插件是基于拦截器机制,在执行SQL前解析和修改SQL。
但对于数据权限的控制维度和实现思路,都是一致的,基于部门维度,控制点放在角色上,因此角色爆炸问题和权限扩大问题,也都存在。

开源平台资料

平台名称:一二三开发平台
简介: 企业级通用开发平台
设计资料:csdn专栏
开源地址:Gitee
开源协议:MIT
如果您在阅读本文时获得了帮助或受到了启发,希望您能够喜欢并收藏这篇文章,为它点赞~
请在评论区与我分享您的想法和心得,一起交流学习,不断进步,遇见更加优秀的自己!

你可能感兴趣的:(#,数据权限,数据权限,数据权限设计,数据权限插件,Mybatisplus,数据权限实现)