开发者社区 > 博文 > 使用mybatis切片实现数据权限控制
分享
  • 打开微信扫码分享

  • 点击前往QQ分享

  • 点击前往微博分享

  • 点击复制链接

使用mybatis切片实现数据权限控制

  • jd****
  • 2024-07-05
  • IP归属:北京
  • 100浏览

    一、使用方式

    数据权限控制需要对查询出的数据进行筛选,对业务入侵最少的方式就是利用mybatis或者数据库连接池的切片对已有业务的sql进行修改。切片逻辑完成后,仅需要在业务中加入少量标记代码,就可以实现对数据权限的控制。这种修改方式,对老业务的逻辑没有入侵或只有少量入侵,基本不影响老业务的逻辑和可读性;对新业务,业务开发人员无需过多关注权限问题,可以集中精力处理业务逻辑。

    由于部门代码中使用的数据库连接池种类较多,不利于切片控制逻辑的快速完成,而sql拼接的部分基本只有mybatis和java字符串直接拼接两种方式,因此使用mybatis切片的方式来完成数据权限控制逻辑。在mybatis的mapper文件的接口上添加注解,注解中写明需要控制的权限种类、要控制的表名、列名即可控制接口的数据权限。

    由于mybatis的mapper文件中的同一接口在多个地方被调用,有的需要控制数据权限,有的不需要,因此增加一种权限控制方式:通过ThreadLocal传递权限控制规则来控制当前sql执行时控制数据权限。

    权限控制规则格式如下:

    限权规则code1(表名1.字段名1,表名2.字段名2);限权规则code2(表名3.字段名3,表名4.字段名4)

    例如:enterprise(channel.enterprise_code);account(table.column);channel(table3.id)

        上下文传递工具类如下所示,使用回调的方式传递ThreadLocal可以防止使用者忘记清除上下文。


    public class DataAuthContextUtil {
        /**
         * 不方便使用注解的地方,可以直接使用上下文设置数据规则
         */
        private static ThreadLocal<String> useDataAuth = new ThreadLocal<>();
    
        /**
         * 有的sql只在部分情况下需要使用数据权限限制
    
         * 上下文和注解中均可设置数据权限规则,都设置时,上下文中的优先
         *
         * @param supplier
         */
        public static <T> T executeSqlWithDataAuthRule(String rule, Supplier<T> supplier) {
            try {
                useDataAuth.set(rule);
                return supplier.get();
            } finally {
                useDataAuth.remove();
            }
        }
    
        /**
         * 获取数据权限标志
         *
         * @return
         */
        public static String getUseDataAuthRule() {
            return useDataAuth.get();
        }
    }

    二、切片实现流程

    三、其他技术细节

    (1)在切面中获取原始sql

    import lombok.extern.slf4j.Slf4j;
    import org.apache.commons.collections4.CollectionUtils;
    import org.apache.commons.lang3.StringUtils;
    import org.apache.ibatis.cache.CacheKey;
    import org.apache.ibatis.executor.Executor;
    import org.apache.ibatis.mapping.BoundSql;
    import org.apache.ibatis.mapping.MappedStatement;
    import org.apache.ibatis.mapping.SqlSource;
    import org.apache.ibatis.plugin.Interceptor;
    import org.apache.ibatis.plugin.Intercepts;
    import org.apache.ibatis.plugin.Invocation;
    import org.apache.ibatis.plugin.Signature;
    import org.apache.ibatis.reflection.DefaultReflectorFactory;
    import org.apache.ibatis.reflection.MetaObject;
    import org.apache.ibatis.reflection.factory.DefaultObjectFactory;
    import org.apache.ibatis.reflection.wrapper.DefaultObjectWrapperFactory;
    import org.apache.ibatis.session.ResultHandler;
    import org.apache.ibatis.session.RowBounds;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.stereotype.Component;
    import reactor.util.function.Tuple2;
    
    import java.lang.reflect.Method;
    import java.util.HashMap;
    import java.util.List;
    import java.util.Map;
    import java.util.Set;
    
    @Component
    @Intercepts({
    //        @Signature(type = Executor.class, method = "update", args = {MappedStatement.class, Object.class}),
            @Signature(type = Executor.class, method = "query", args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class}),
            @Signature(type = Executor.class, method = "query", args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class, CacheKey.class, BoundSql.class})
    })
    @Slf4j
    public class DataAuthInterceptor implements Interceptor {
    
        @Override
        public Object intercept(Invocation invocation) throws Throwable {
            try {
                MappedStatement mappedStatement = (MappedStatement) invocation.getArgs()[0];
                BoundSql boundSql = mappedStatement.getBoundSql(invocation.getArgs()[1]);
                String sql = boundSql.getSql();
            } catch (Exception e) {
                log.error("数据权限添加出错,当前sql未加数据权限限制!", e);
                throw e;
            }
            return invocation.proceed();
        }
    }

    (2)将权限项加入原始sql中

    使用druid附带的ast解析功能修改sql,代码如下

    /**
         * 权限限制写入sql
         *
         * @param sql
         * @param tableAuthMap key:table value1:column value2:values权限项
         * @return
         */
        public static StringBuilder addAuthLimitToSql(String sql, Map<String, Tuple2<String, Set<String>>> tableAuthMap) {
            List<SQLStatement> stmtList = SQLUtils.parseStatements(sql, "mysql");
            StringBuilder authSql = new StringBuilder();
            for (SQLStatement stmt : stmtList) {
                stmt.accept(new MySqlASTVisitorAdapter() {
                    @Override
                    public boolean visit(MySqlSelectQueryBlock x) {
                        SQLTableSource from = x.getFrom();
                        Set<String> tableList = new HashSet<>();
                        getTableList(from, tableList);
                        for (String tableName : tableList) {
                            if (tableAuthMap.containsKey(tableName)) {
                                x.addCondition(tableName + "in (...略)");
                            }
                        }
                        return true;
                    }
                });
                authSql.append(stmt);
            }
            return authSql;
        }
        
        private static void getTableList(SQLTableSource from, Set<String> tableList) {
            if (from instanceof SQLExprTableSource) {
                SQLExprTableSource tableSource = (SQLExprTableSource) from;
                String name = tableSource.getTableName().replace("`", "");
                tableList.add(name);
                String alias = tableSource.getAlias();
                if (StringUtils.isNotBlank(alias)) {
                    tableList.add(alias.replace("`", ""));
                }
            } else if (from instanceof SQLJoinTableSource) {
                SQLJoinTableSource joinTableSource = (SQLJoinTableSource) from;
                getTableList(joinTableSource.getLeft(), tableList);
                getTableList(joinTableSource.getRight(), tableList);
            } else if (from instanceof SQLSubqueryTableSource) {
                SQLSubqueryTableSource tableSource = (SQLSubqueryTableSource) from;
                tableList.add(tableSource.getAlias().replace("`", ""));
            } else if (from instanceof SQLLateralViewTableSource) {
                log.warn("SQLLateralView不用处理");
            } else if (from instanceof SQLUnionQueryTableSource) {
                //union 不需要处理
                log.warn("union不用处理");
            } else if (from instanceof SQLUnnestTableSource) {
                log.warn("Unnest不用处理");
            } else if (from instanceof SQLValuesTableSource) {
                log.warn("Values不用处理");
            } else if (from instanceof SQLWithSubqueryClause) {
                log.warn("子查询不用处理");
            } else if (from instanceof SQLTableSourceImpl) {
                log.warn("Impl不用处理");
            }
        }
    }

    (3)将修改过后的sql写回mybatis

            MappedStatement ms = (MappedStatement) invocation.getArgs()[0];
            BoundSql boundSql = ms.getBoundSql(invocation.getArgs()[1]);
            // 组装 MappedStatement
            MappedStatement.Builder builder = new MappedStatement.Builder(ms.getConfiguration(), ms.getId(), new MySqlSource(boundSql), ms.getSqlCommandType());
            builder.resource(ms.getResource());
            builder.fetchSize(ms.getFetchSize());
            builder.statementType(ms.getStatementType());
            builder.keyGenerator(ms.getKeyGenerator());
            if (ms.getKeyProperties() != null && ms.getKeyProperties().length != 0) {
                StringBuilder keyProperties = new StringBuilder();
                for (String keyProperty : ms.getKeyProperties()) {
                    keyProperties.append(keyProperty).append(",");
                }
                keyProperties.delete(keyProperties.length() - 1, keyProperties.length());
                builder.keyProperty(keyProperties.toString());
            }
            builder.timeout(ms.getTimeout());
            builder.parameterMap(ms.getParameterMap());
            builder.resultMaps(ms.getResultMaps());
            builder.resultSetType(ms.getResultSetType());
            builder.cache(ms.getCache());
            builder.flushCacheRequired(ms.isFlushCacheRequired());
            builder.useCache(ms.isUseCache());
            MappedStatement newMappedStatement = builder.build();
            MetaObject metaObject = MetaObject.forObject(newMappedStatement, new DefaultObjectFactory(), new DefaultObjectWrapperFactory(), new DefaultReflectorFactory());
            metaObject.setValue("sqlSource.boundSql.sql", newSql);
            invocation.getArgs()[0] = newMappedStatement;


    参考文章: https://blog.csdn.net/e_anjing/article/details/79102693

    文章数
    1
    阅读量
    0

    作者其他文章