写点什么

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

  • 2024-07-08
    北京
  • 本文字数:4323 字

    阅读完需:约 14 分钟

一、使用方式

数据权限控制需要对查询出的数据进行筛选,对业务入侵最少的方式就是利用 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})})@Slf4jpublic 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

发布于: 刚刚阅读数: 4
用户头像

拥抱技术,与开发者携手创造未来! 2018-11-20 加入

我们将持续为人工智能、大数据、云计算、物联网等相关领域的开发者,提供技术干货、行业技术内容、技术落地实践等文章内容。京东云开发者社区官方网站【https://developer.jdcloud.com/】,欢迎大家来玩

评论

发布
暂无评论
使用mybatis切片实现数据权限控制_京东科技开发者_InfoQ写作社区