Common data permissions limit SQL automatic injection tool sharing (source code attached)

Copyright notice: This article was originally created by the blogger keep, and please indicate the source for reprinting.
Original address: https://blog.csdn.net/qq_38688267/article/details/114134664

Background introduction

  As we all know, it is 系统权限divided into 功能权限sum 数据权限, which 功能权限determines which interfaces and functions the user can access, 数据权限and determines which data the user can see. Most systems will use this function, it can be said to be very common.

  What to do with data permission restrictions? Under normal circumstances, everyone adds the user's permission conditions to the SQL that needs to be restricted. The author has also implemented this before.
  Although the SQL for these permission conditions was encapsulated at the time, and every time I used it, I only needed to quote it, but I still felt very troublesome. I wondered if I could let the program help me realize the data permission restriction SQL injection? (I'm really a clever person who likes to be "lazy")

  With this in mind, the author began to fiddle, fiddle after a while, the author of the package based Mybatis 通用数据权限限制SQL工具V1.0version fiddle finally came out!


Effect demonstration

Insert picture description here
Insert picture description here

  After reading the above effect, is it strange to everyone? sys_roleHow did this watch come from? df_alsWhere did the alias come from? ON sr.tenant_id = df_als.tenant_idhow come? sr.id IN ('asdf', 'asdff')How did it come?

  It's all configured! In order to be versatile enough, many things are configurable, such as:

  • Which tables, methods, and classes need to be restricted, and which ones are not configurable
  • Which tables are used to limit the configuration
  • Association fields and condition fields can be configured
  • Where condition data value source can be configured

  there are more:

  • Multiple SQL processors can be configured
  • Multiple processor execution strategies can be configured


Core code introduction

To realize this function, there are several points:

  • Need to mark and get what are the methods that need to be restricted
  • To achieve the interception of the above method
  • Implement SQL modification

The following one by one introduces the author's realization method.

Need to restrict method marking and acquisition

  The mark is very simple, just use a comment. The author used the mapper scanning work of mybatis, and rewritten its method to help us check whether there are mark annotations by the way.

/** 
 * 数据权限限制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);
    }
}

Implement the interception of the marking method

  The interception is also relatively simple, we directly implement the interception interface provided by 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;
    }
}

  

Implement SQL modification

  It is not realistic to modify SQL by hand. There are too many situations to consider. The author uses JSqlParsertools to modify SQL. Part of the code is as follows:


    /**
     * 进行数据权限限制的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;
    }

Source code sharing

Project address: https://gitee.com/zengzefeng/easy-frame

to sum up

  This feature is only the first version, there are definitely many shortcomings and ill-considered places, please give me your advice. We will continue to optimize and iterate in the future, hoping to become a spring-boot-starter, haha.

Guess you like

Origin blog.csdn.net/qq_38688267/article/details/114134664