0
  • 聊天消息
  • 系统消息
  • 评论与回复
登录后你可以
  • 下载海量资料
  • 学习在线课程
  • 观看技术视频
  • 写文章/发帖/加入社区
会员中心
创作中心

完善资料让更多小伙伴认识你,还能领取20积分哦,立即完善>

3天内不再提示

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

京东云 来源:jf_75140285 作者:jf_75140285 2024-07-09 17:26 次阅读

一、使用方式

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

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

wKgZomaLyPGADDxAAAEr4RL-Yl0024.png



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

wKgaomaLyPKAXPzWAAC3LRLJclU836.png



权限控制规则格式如下:

限权规则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();
    }
}

二、切片实现流程

wKgZomaLyPKANAOVAAEBobDhJAA083.png



三、其他技术细节

(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

审核编辑 黄宇

声明:本文内容及配图由入驻作者撰写或者入驻合作网站授权转载。文章观点仅代表作者本人,不代表电子发烧友网立场。文章及其配图仅供工程师学习之用,如有内容侵权或者其他违规问题,请联系本站处理。 举报投诉
  • 权限控制
    +关注

    关注

    0

    文章

    5

    浏览量

    6374
  • mybatis
    +关注

    关注

    0

    文章

    58

    浏览量

    6688
收藏 人收藏

    评论

    相关推荐

    基于SpringBoot mybatis方式的增删改查实现

    SpringBoot mybatis方式实现增删改查
    发表于 06-18 16:56

    数据库整合Mybatis框架

    微服务 SpringBoot 20(九):整合Mybatis
    发表于 07-16 11:03

    基于规则引擎的实例级权限控制研究

    该文通过比较常用的权限控制实现方法,提出了基于规则引擎的实例级权限控制框架,能灵活实现复杂的安
    发表于 09-03 12:12 16次下载

    基于RBAC的限制约束在权限控制中的实现

    本文针对当前权限控制框架在权限控制数据保护方面存在的问题,提出一种新的权限
    发表于 01-15 16:08 6次下载

    PLM系统中数据权限控制研究

    基于PLM系统中影响数据权限的4种因素,提出了基于权限规则的动态权限控制机制。为了使数据对象的生
    发表于 03-07 15:17 0次下载
    PLM系统中<b class='flag-5'>数据</b><b class='flag-5'>权限</b><b class='flag-5'>控制</b>研究

    基于ThinkPHP的权限控制模块的设计与实现许宏云

    基于ThinkPHP的权限控制模块的设计与实现_许宏云
    发表于 03-17 08:00 0次下载

    MyBatis实现原理

    本文主要详细介绍了MyBatis实现原理。mybatis底层还是采用原生jdbc来对数据库进行操作的,只是通过 SqlSessionFactory,SqlSession Execut
    的头像 发表于 02-24 11:25 6399次阅读
    <b class='flag-5'>MyBatis</b>的<b class='flag-5'>实现</b>原理

    Fluent Mybatis、原生MybatisMybatis Plus对比

    使用fluent mybatis可以不用写具体的xml文件,通过java api可以构造出比较复杂的业务sql语句,做到代码逻辑和sql逻辑的合一。不再需要在Dao中组装查询或更新操作,在xml或
    的头像 发表于 09-15 15:41 1325次阅读

    权限系统中的数据权限设计经验分享

    RBAC模型 数据权限 数据规则 关联资源、用户 继续优化 小结 在项目实际开发中我们不光要控制一个用户能访问哪些资源,还需要控制用户只能访
    的头像 发表于 10-11 17:50 1500次阅读

    SpringBoot+Mybatis如何实现流式查询?

    使用mybatis作为持久层的框架时,通过mybatis执行查询数据的请求执行成功后,mybatis返回的结果集不是一个集合或对象,而是一个迭代器,可以通过遍历迭代器来取出结果集
    的头像 发表于 06-12 09:57 971次阅读

    基于Mybatis拦截器实现数据范围权限

    前端的菜单和按钮权限都可以通过配置来实现,但很多时候,后台查询数据数据权限需要通过手动添加SQL来
    的头像 发表于 06-20 09:57 1124次阅读
    基于<b class='flag-5'>Mybatis</b>拦截器<b class='flag-5'>实现</b><b class='flag-5'>数据</b>范围<b class='flag-5'>权限</b>

    如何实现基于Mybatis拦截器实现数据范围权限呢?

    前端的菜单和按钮权限都可以通过配置来实现,但很多时候,后台查询数据数据权限需要通过手动添加SQL来
    的头像 发表于 06-20 09:59 1017次阅读
    如何<b class='flag-5'>实现</b>基于<b class='flag-5'>Mybatis</b>拦截器<b class='flag-5'>实现</b><b class='flag-5'>数据</b>范围<b class='flag-5'>权限</b>呢?

    如何利用MyBatis Plus去实现数据权限控制呢?

    平时开发中遇到根据当前用户的角色,只能查看数据权限范围的数据需求。列表实现方案有两种,一是在开发初期就做好判断赛选,但如果这个需求是中途加的,或不希望每个接口都加一遍,就可以方案二加拦
    的头像 发表于 08-23 10:40 930次阅读
    如何利用<b class='flag-5'>MyBatis</b> Plus去<b class='flag-5'>实现</b><b class='flag-5'>数据</b><b class='flag-5'>权限</b><b class='flag-5'>控制</b>呢?

    mybatis的dao能重载吗

    不同需求的数据操作。 重载是指在同一个类中定义了多个方法,它们具有相同的名称但具有不同的参数。重载允许使用相同的方法名来处理不同类型和数量的参数,以提供更加灵活的操作。 在MyBatis的DAO中,我们可以通过重载方法来实现不同
    的头像 发表于 12-03 11:51 1021次阅读

    鸿蒙开发组件:DataAbility权限控制

    DataAbility提供数据服务,并不是所有的Ability都有权限读写它,DataAbility有一套权限控制机制来保证数据安全。分为静
    的头像 发表于 06-21 10:30 197次阅读
    鸿蒙开发组件:DataAbility<b class='flag-5'>权限</b><b class='flag-5'>控制</b>