使用mybaits插件实现数据权限

在业务系统开发中,数据权限的需求是少不了的.大多数都是根据操作人的角色限制数据范围,例如只能查询某些部门,类型或者用户的数据.
例如,查询订单sql

select o.id,o.order_no,o.type,o.salesman_id ,s.salesman 
from  test_order o
join test_salesman s on o.salesman_id=s.id

如果要限制只能查询某个类型和某个业务员的数据,那得挨个加上where条件,如

select o.id,o.order_no,o.type,o.salesman_id ,s.salesman 
from  test_order o 
join test_salesman s on o.salesman_id=s.id 
WHERE  o.type IN ('1') and s.salesman IN ('A', 'B')

这样的做法工作量大,容易出错,对系统的入侵性高,后续修改和维护非常困难,所以应该在框架层面上实现这样的需求,可以利用mybaits的Interceptor插件接口实现.
数据权限实体结构如下

@Data
public class DataPermission {
    //数据库表名
    private String tableName;
    //需要限制数据范围的列名
    private String columnName;
    //限制的数据列表
    private List<String> inValueList;
}

使用ThreadLocal线程变量存放数据权限列表,并且mock一些数据,如下

public class DataPermissionUtils {

    private static ThreadLocal<List<DataPermission>> dataPermissionThreadLocal = new ThreadLocal<>();

    public static List<DataPermission> getDataPermissionList() {
        List<DataPermission> list = dataPermissionThreadLocal.get();
        if (list == null) {
            DataPermission dataPermission = new DataPermission();
            dataPermission.setColumnName("salesman");
            dataPermission.setTableName("test_salesman");
            dataPermission.setInValueList(Arrays.asList("A", "B"));

            DataPermission dataPermission1 = new DataPermission();
            dataPermission1.setColumnName("type");
            dataPermission1.setTableName("test_order");
            dataPermission1.setInValueList(Arrays.asList("1"));
            list = Arrays.asList(dataPermission, dataPermission1);
            dataPermissionThreadLocal.set(list);
        }
        return list;
    }
}

编写mybaits插件

在DataPermissionInterceptor插件中获取sql,通过CCJSqlParserUtil类解析,在处理子查询,join等语句中加入权限sql,如下

@Component
@Intercepts({
        @Signature(type = StatementHandler.class, method = "prepare", args = {Connection.class, Integer.class})
})
public class DataPermissionInterceptor implements Interceptor {

    @Override
    public Object intercept(Invocation invocation) throws Throwable {

        StatementHandler statementHandler = getRealTarget(invocation.getTarget());
        MetaObject metaObject = SystemMetaObject.forObject(statementHandler);
        MappedStatement mappedStatement = (MappedStatement) metaObject.getValue("delegate.mappedStatement");

        //判断是否select语句和是否有数据权限控制
        if (mappedStatement.getSqlCommandType().equals(SqlCommandType.SELECT)) {
            Select selectStatement = (Select) CCJSqlParserUtil.parse(statementHandler.getBoundSql().getSql());
            //开始处理sql
            processSelectBody(selectStatement.getSelectBody());
            metaObject.setValue("delegate.boundSql.sql", selectStatement.toString());
        }
        return invocation.proceed();
    }

    public static <T> T getRealTarget(Object target) {
        if (Proxy.isProxyClass(target.getClass())) {
            MetaObject metaObject = SystemMetaObject.forObject(target);
            return getRealTarget(metaObject.getValue("h.target"));
        }
        return (T) target;
    }

    public void processSelectBody(SelectBody selectBody) {
        if (selectBody instanceof PlainSelect) {
            processPlainSelect((PlainSelect) selectBody);
        } else if (selectBody instanceof WithItem) {
            WithItem withItem = (WithItem) selectBody;
            if (withItem.getSelectBody() != null) {
                processSelectBody(withItem.getSelectBody());
            }
        } else {
            SetOperationList operationList = (SetOperationList) selectBody;
            if (operationList.getSelects() != null && operationList.getSelects().size() > 0) {
                operationList.getSelects().forEach(x -> processSelectBody(x));
            }
        }
    }

    protected void processPlainSelect(PlainSelect plainSelect) {
        FromItem fromItem = plainSelect.getFromItem();
        //子查询
        processFromItem(fromItem);
        //处理where条件
        plainSelect.setWhere(builderExpression(plainSelect.getWhere(), fromItem));
        //处理关联
        List<Join> joins = plainSelect.getJoins();
        if (joins != null && joins.size() > 0) {
            joins.forEach(j -> {
                processJoin(j);
            });
        }
    }

    /**
     * 处理条件
     */
    protected Expression builderExpression(Expression expression, FromItem fromItem) {

        if (expression != null) {
            if (expression instanceof FromItem) {
                processFromItem((FromItem) expression);
            }
            if (expression instanceof BinaryExpression) {
                BinaryExpression binaryExpression = (BinaryExpression) expression;
                builderExpression(binaryExpression.getLeftExpression(), null);
                builderExpression(binaryExpression.getRightExpression(), null);
            }
            if (expression instanceof InExpression) {
                InExpression inExp = (InExpression) expression;
                ItemsList rightItems = inExp.getRightItemsList();
                if (rightItems instanceof SubSelect) {
                    processSelectBody(((SubSelect) rightItems).getSelectBody());
                }
            }
        }

        if (fromItem instanceof Table) {
            Table table = (Table) fromItem;
            DataPermission dataPermission = getDataPermission(table);
            if (dataPermission != null) {
                InExpression inExpression = new InExpression();
                inExpression.setLeftExpression(this.getAliasColumn(table, dataPermission.getColumnName()));
                MultiExpressionList multiExpressionList = new MultiExpressionList();
                multiExpressionList.addExpressionList(dataPermission.getInValueList().stream().map(x -> new StringValue(x)).collect(Collectors.toList()));
                inExpression.setRightItemsList(multiExpressionList);

                if (expression == null) {
                    return inExpression;
                } else {
                    if (expression instanceof OrExpression) {
                        return new AndExpression(inExpression, new Parenthesis(expression));
                    } else {
                        return new AndExpression(inExpression, expression);
                    }
                }
            }
        }

        return expression;
    }


    /**
     * 处理FromItem,如果是子查询等,则递归处理
     */
    protected void processFromItem(FromItem fromItem) {
        if (fromItem instanceof SubJoin) {
            SubJoin subJoin = (SubJoin) fromItem;
            if (subJoin.getJoinList() != null) {
                subJoin.getJoinList().forEach(this::processJoin);
            }
            if (subJoin.getLeft() != null) {
                processFromItem(subJoin.getLeft());
            }
        } else if (fromItem instanceof SubSelect) {
            SubSelect subSelect = (SubSelect) fromItem;
            if (subSelect.getSelectBody() != null) {
                processSelectBody(subSelect.getSelectBody());
            }
        } else if (fromItem instanceof ValuesList) {

        } else if (fromItem instanceof LateralSubSelect) {
            LateralSubSelect lateralSubSelect = (LateralSubSelect) fromItem;
            if (lateralSubSelect.getSubSelect() != null) {
                SubSelect subSelect = lateralSubSelect.getSubSelect();
                if (subSelect.getSelectBody() != null) {
                    processSelectBody(subSelect.getSelectBody());
                }
            }
        }
    }

    /**
     * 处理联接语句
     */
    protected void processJoin(Join join) {
        processFromItem(join.getRightItem());
        join.setOnExpression(builderExpression(join.getOnExpression(), join.getRightItem()));
    }

    // 拼接字段名
    protected Column getAliasColumn(Table table, String columnName) {
        StringBuilder column = new StringBuilder();
        if (null == table.getAlias()) {
            column.append(table.getName());
        } else {
            column.append(table.getAlias().getName());
        }
        column.append(".");
        column.append(columnName);
        return new Column(column.toString());
    }


    private DataPermission getDataPermission(Table fromTable) {
        List<DataPermission> list = DataPermissionUtils.getDataPermissionList();
        DataPermission dataPermission = list.stream().filter(x -> Objects.equals(x.getTableName(), fromTable.getName())).findFirst().orElse(null);
        return dataPermission;
    }

测试一下一条极端的sql语句,如

   select o.id,o.order_no as orderNo,o.type,o.salesman_id as salesmanId,s.salesman
        from (select * from  test_order) o
        join test_order oo on o.id=oo.id
        join test_salesman s on o.salesman_id=s.id and s.id in (select id from test_salesman)
        join (select  * from test_salesman) ss on o.salesman_id=ss.id
        where  o.salesman_id in (select id from test_salesman)

使用插件后,自动在各个查询和子查询中加入对应的in语句,实际执行的sql如下

SELECT o.id, o.order_no AS orderNo, o.type, o.salesman_id AS salesmanId, s.salesman 
FROM (SELECT * FROM test_order WHERE test_order.type IN ('1')) o 
JOIN test_order oo ON oo.type IN ('1') AND o.id = oo.id 
JOIN test_salesman s ON s.salesman IN ('A', 'B') AND o.salesman_id = s.id AND s.id IN (SELECT id FROM test_salesman WHERE test_salesman.salesman IN ('A', 'B')) 
JOIN (SELECT * FROM test_salesman WHERE test_salesman.salesman IN ('A', 'B')) ss ON o.salesman_id = ss.id 
WHERE o.salesman_id IN (SELECT id FROM test_salesman WHERE test_salesman.salesman IN ('A', 'B'))

上面插件还存在一个问题就是,插件启用后会对所有sql加入对应的权限sql,事实上有些场景的sql可能不需要数据权限,所以还需要定制一个注解,加上此注解的查询方法,忽略权限sql处理,注解如下

@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE, ElementType.METHOD})
public @interface DataScope {
    boolean enabled() default true;
}

DataPermissionInterceptor加入对注解的处理

 @Override
    public Object intercept(Invocation invocation) throws Throwable {

        StatementHandler statementHandler = getRealTarget(invocation.getTarget());
        MetaObject metaObject = SystemMetaObject.forObject(statementHandler);
        MappedStatement mappedStatement = (MappedStatement) metaObject.getValue("delegate.mappedStatement");

        //判断是否select语句和是否有数据权限控制
        if (mappedStatement.getSqlCommandType().equals(SqlCommandType.SELECT) && isDataScope(mappedStatement)) {
            Select selectStatement = (Select) CCJSqlParserUtil.parse(statementHandler.getBoundSql().getSql());
            //开始处理sql
            processSelectBody(selectStatement.getSelectBody());
            metaObject.setValue("delegate.boundSql.sql", selectStatement.toString());
        }
        return invocation.proceed();
    }

    private Boolean isDataScope(MappedStatement mappedStatement) {
        try {
            String mappedStatementId = mappedStatement.getId();
            int lastDotIndex = mappedStatementId.lastIndexOf(".");//id是由类名+方法名组成
            String className = mappedStatementId.substring(0, lastDotIndex);//找出类名
            String methodName = mappedStatementId.substring(lastDotIndex + 1);//找出方法名
            //在注册的mapper中找出对应的类
            Class aClass = mappedStatement.getConfiguration().getMapperRegistry().getMappers().stream().filter(x -> x.getName().equals(className)).findFirst().orElse(null);

            if (aClass != null) {
                Method method = null;
                for (Method item : aClass.getMethods()) {
                    if (item.getName().equals(methodName)) {
                        method = item;
                        break;
                    }
                }
                //查找DataScope注解,默认是过滤数据
                if (method != null) {
                    DataScope dataScopeAnnotation = AnnotationUtils.getAnnotation(method, DataScope.class);
                    if (dataScopeAnnotation != null) {
                        return dataScopeAnnotation.enabled();
                    }
                    return true;
                }
            }
        } catch (Exception ex) {
            ex.printStackTrace();
        }
        return false;
    }

注解使用,如下

@Mapper
public interface SalesmanMapper {
    @DataScope(enabled = false)
    List<SalesmanVO> list();
}

题外话

  • 上面的插件可以实现数据权限的过滤,但是数据权限还有的场景是,不能查看某些列,所以我们还可以对查询的列进行处理

  • 插件只处理了select类型的语句,按照这样的思路,也处理一下其他类型的sql,例如insert语句,默认插入某些列,例如租户之类的

  • 上文利用ThreadLocal传递模拟的数据权限,实际开发中,这里的来源是根据登陆用户的角色来读取的,可以是读数据库,也可以读取redis之类的,而数据库或者redis的数据是怎么产生的,那就需要另外做功能界面来维护了

  • 0
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值