通用数据权限限制SQL自动注入工具分享(附源码)

版权说明: 本文由博主keep丶原创,转载请注明出处。
原文地址: https://blog.csdn.net/qq_38688267/article/details/114134664

背景介绍

  众所周知,系统权限分为功能权限数据权限功能权限决定用户能访问哪些界面有哪些功能,数据权限则决定用户能看到哪些数据。绝大部分系统都会用到这个功能,可以说是非常常见了。

  数据权限限制该怎么做呢? 一般情况下大家都是在需要限制的sql中加上该用户的权限条件,作者之前也是这么实现的。
  虽然当时把这些权限条件SQL都封装了,每次使用的时候只要引用就行,但是还是感觉很麻烦,就想着能不能让程序来帮我实现这些数据权限限制SQL的注入呢?(我可真是个喜欢"偷懒"的小聪明呢)

  有了这个想法,作者就开始捣鼓了,经过一阵捣鼓后,作者封装的基于Mybatis的 通用数据权限限制SQL工具V1.0 版本总算捣鼓出来了!


效果演示

在这里插入图片描述
在这里插入图片描述

  看完上面的效果,各位是不是很奇怪? sys_role这张表怎么来的?df_als别名怎么来的?ON sr.tenant_id = df_als.tenant_id怎么来的?sr.id IN ('asdf', 'asdff')又是怎么来的?

  全都是配置出来的! 为了足够通用,很多东西都是可配的,如:

  • 哪些表、方法、类需要限制,哪些不用可配
  • 用哪些表来限制可配
  • 关联关系字段、条件字段可配
  • where条件数据值来源可配

  还有更多:

  • 可配置多个SQL处理器
  • 多个处理器执行策略可配


核心代码介绍

要实现这个功能,有几个要点:

  • 需要标记并获取哪些是需要限制的方法
  • 实现上述方法的拦截
  • 实现SQL修改

下面一一为大家介绍作者的实现方式。

需要限制的方法标记和获取

  标记很简单,用个注解就好了。获取作者利用了mybatis的mapper扫描工作,重写其方法让其顺便帮我们检查下是否带有标记注解。

/** 
 * 数据权限限制SqlInjector
 * <p>
 * 重写mp的mapper扫描逻辑
 * 在扫描mapper后检查是否需要数据权限限制并缓存这些信息
 *
 * @author zzf
 * @date 2021/2/5 15:02
 */
@Slf4j
public class DataAuthSqlInjector extends DefaultSqlInjector {

    @Override
    public void inspectInject(MapperBuilderAssistant builderAssistant, Class<?> mapperClass) {
        String resource = ResourceUtils.getResourceKey(mapperClass);

        Set<String> needAuthMethodSet = new HashSet<>();
        Set<String> notNeedAuthMethodSet = new HashSet<>();

        if (mapperClass.isAnnotationPresent(DataAuth.class)) {
            DataAuthCache.addNeedAuthResource(resource);

            for (Method method : mapperClass.getMethods()) {
                needAuthMethodSet.add(method.getName());
            }
        }

        for (Method method : mapperClass.getMethods()) {
            if (method.isAnnotationPresent(DataAuth.class)) {
                needAuthMethodSet.add(method.getName());
            }
            if (method.isAnnotationPresent(DataAuthIgnore.class)) {
                notNeedAuthMethodSet.add(method.getName());
            }
        }
        if (needAuthMethodSet.size() > 0) {
            DataAuthCache.addNeedAuthMethod(resource, needAuthMethodSet);
            DataAuthCache.addResource2EntityClassMap(resource, extractModelClass(mapperClass));
        }
        if (notNeedAuthMethodSet.size() > 0) {
            DataAuthCache.addNotNeedAuthMethod(resource, notNeedAuthMethodSet);
        }
        super.inspectInject(builderAssistant, mapperClass);
    }
}

实现标记方法的拦截

  拦截也比较简单,我们直接实现Mybatis Plus提供的拦截接口:


/**
 * 数据权限拦截器
 *
 * @author zzf
 * @date 11:05 2021/2/4
 */
@Slf4j
public class DataAuthInterceptor implements InnerInterceptor {

    //限制策略
    private final DataAuthStrategy dataAuthStrategy;

    private final Set<String> needAuthId = new TreeSet<>();
    private final Set<String> notNeedAuthId = new TreeSet<>();

    public DataAuthInterceptor(DataAuthStrategy dataAuthStrategy) {
        this.dataAuthStrategy = dataAuthStrategy;
    }

    @Override
    public void beforeQuery(Executor executor, MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) throws SQLException {
        String id = ms.getId();

        String resource = ResourceUtils.getResourceKey(ms.getResource());

        String methodName;
        if (id.contains(".")) {
            methodName = id.substring(id.lastIndexOf(".") + 1);
        } else {
            methodName = id;
        }
        if (needAuthCheck(resource, methodName, id)) {
            log.warn("sql before parsed: " + boundSql.getSql());
            Class<?> entityClass = DataAuthCache.getEntityClassByResource(resource);
            try {
                Select selectStatement = (Select) CCJSqlParserUtil.parse(boundSql.getSql());
                dataAuthStrategy.doParse(selectStatement, entityClass);

                ReflectUtil.setFieldValue(boundSql, "sql", selectStatement.toString());
                log.warn("sql after parsed: " + boundSql.getSql());

            } catch (JSQLParserException e) {
                throw new SQLException("sql role limit fail: sql parse fail.");
            }

        }
    }


    /**
     * 判断是否需要数据权限限制
     */
    private boolean needAuthCheck(String resource, String methodName, String id) {
        if (needAuthId.contains(id)) {
            return true;
        }
        if (notNeedAuthId.contains(id)) {
            return false;
        }
        boolean result;
        if (DataAuthCache.isNeedAuthResource(resource)) {
            result = !DataAuthCache.isNotNeedAuthMethod(resource, methodName);
        } else {
            result = DataAuthCache.isNeedAuthMethod(resource, methodName);
        }

        if (result) {
            needAuthId.add(id);
        } else {
            notNeedAuthId.add(id);
        }
        return result;
    }
}

  

实现SQL修改

  自己手写SQL修改是不太现实的,要考虑的情况太多了,作者使用JSqlParser工具来进行SQL修改,部分代码如下:


    /**
     * 进行数据权限限制的SQL处理
     */
    public SqlHandlerResult doParse(Select selectStatement, Class<?> entityClass) {
        T data = dataGetter.getData();
        PlainSelect selectBody = (PlainSelect) selectStatement.getSelectBody();
        //判断数据是否为空,且为空时是否处理
        if (data == null) {
            if (parseIfDataAbsent()) {
                //空数据权限处理
                EqualsTo equalsTo = new EqualsTo();
                equalsTo.setRightExpression(new LongValue(1));
                equalsTo.setLeftExpression(new LongValue(0));
                //在where子句中添加 1 = 0 条件, 并 and 原有条件
                AndExpression andExpression = new AndExpression(equalsTo, selectBody.getWhere());
                selectBody.setWhere(andExpression);
            }
            return SqlHandlerResult.nonData(parseIfDataAbsent());
        } else {
            if (dataIsFull(data)) {
                // 拥有全部权限,此处不对SQL做任何处理
                return SqlHandlerResult.hadAll();
            } else {
                // 正常处理
                Table fromItem = (Table) selectBody.getFromItem();
                aliasHandle(selectBody, fromItem);
                joinHandle(entityClass, selectBody, fromItem);
                whereHandle(selectBody);
            }
        }
        return SqlHandlerResult.handle();
    }

    /**
     * 别名处理
     * 如果from的表没有别名,手动给他加个别名,并给所有 查询列和 where条件中的列增加别名前缀
     */
    public void aliasHandle(PlainSelect selectBody, Table fromItem) {
        if (fromItem.getAlias() == null || fromItem.getAlias().getName() == null) {
            fromItem.setAlias(new Alias(DataAuthConstants.DEFAULT_ALIAS));
            selectBody.getSelectItems().forEach(s ->
                    s.accept(new ExpressionVisitorAdapter() {
                        @Override
                        public void visit(Column column) {
                            column.setTable(fromItem);
                        }
                    })
            );

            Expression where = selectBody.getWhere();
            if (where != null) {
                where.accept(new ExpressionVisitorAdapter() {
                    @Override
                    public void visit(Column column) {
                        column.setTable(fromItem);
                    }
                });
            }
        }
    }

    /**
     * JOIN子句处理
     * <p>
     * 将数据限制表通过JOIN的方式加入
     */
    public void joinHandle(Class<?> entityClass, PlainSelect selectBody, Table fromItem) {
        // 假如要实现 LEFT JOIN t2 ON t2.id2 = t1.id1
        Table limitTable = getLimitTable();
        Join join = new Join();// JOIN
        join.setLeft(true);// LEFT JOIN
        join.setRightItem(limitTable);// LEFT JOIN t2

        EqualsTo equalsTo = new EqualsTo();// =

        Column limitRelationColumn = new Column(relationColumnName);// id2
        limitRelationColumn.setTable(limitTable);// t2.id2

        Column targetRelationColumn = new Column(DataAuthCache.getFieldName(getId(), entityClass));// id1
        targetRelationColumn.setTable(fromItem);// t1.id1

        equalsTo.setLeftExpression(limitRelationColumn);
        equalsTo.setRightExpression(targetRelationColumn);// t2.id2 = t1.id1
        join.setOnExpression(equalsTo);// LEFT JOIN t2 ON t2.id2 = t1.id1

        List<Join> joins = selectBody.getJoins();
        if (joins == null) {
            selectBody.setJoins(Collections.singletonList(join));
        } else {
            joins.add(join);
        }
    }

    /**
     * WHERE子句处理
     * <p>
     * 添加数据权限条件
     */
    public void whereHandle(PlainSelect selectBody) {
        Column whereColumn = new Column(whereColumnName);
        whereColumn.setTable(getLimitTable());

        setOperatorValue();

        AndExpression andExpression = new AndExpression(operator, selectBody.getWhere());
        selectBody.setWhere(andExpression);
    }

    protected Table getLimitTable() {
        Table limitTable = new Table(this.tableName);
        limitTable.setAlias(new Alias(this.tableAlias));
        return limitTable;
    }

源代码分享

项目地址:https://gitee.com/zengzefeng/easy-frame

总结

  该功能只是初版,肯定还有很多不足和考虑不周的地方,还请各位大佬多多指点。后续还会继续优化迭代,希望能打造成一个spring-boot-starter,哈哈。

  • 4
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 3
    评论
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值